🎬 第十二章 各种开发语言调用FFmpeg命令行
📄 一句话概括

Python 只有一个正确答案:subprocess.run(列表, capture_output=True)。 但它有四个默认值需要你显式改掉,否则迟早出事。

📝 本节和前面几节的关系

这一章最后一节,也是最实用的一节。 前面讲的「传参、等、收输出」在 Python 里都有现成答案, 但默认值不是为「调 ffmpeg」这个场景设计的。

一、三种写法,能力一层比一层强

① os.system(命令):只返回一个状态码。 输出直接打到屏幕,你拿不到。 它连「成功」都要你自己移位判断(Linux 上)。 只适合「跑一下,成不成无所谓」。

② subprocess.run(字符串, shell=True):能拿输出与返回码。 但要多经一层 shell,参数又会被解析一遍。 除非你要用管道、通配符,否则没理由用它。

③ subprocess.run(列表, capture_output=True):参数数组直传。 没有 shell 这一层,引号问题不存在。这是唯一推荐的写法。

三者的差别不在「能不能跑」,在「旁边那两件事处理得干不干净」。 第三种把输出、返回码、退出码全给你,而且不引入额外解析。

二、四个默认值要显式改掉

subprocess.run 的默认值是为「跑个短命令」设计的,调 ffmpeg 得改四处:

参数 默认值 该改成


shell False 保持 False,传列表 stdin 继承父进程 DEVNULL timeout 不设 设一个上限 encoding 依 locale "utf-8" + errors

stdin 这一项最容易被忽略。 不指定它,子进程就继承你的标准输入; ffmpeg 默认会去读它,于是「后台跑时被挂起」或者「抢走父进程的输入」都可能发生。 stdin=subprocess.DEVNULL 是比 -nostdin 更彻底的一层保险。

encoding 这一项在中文 Windows 上一定撞。 默认按 locale 走,而中文 Windows 的 locale 是 cp936(GBK); ffmpeg 输出的是 UTF-8,解码那一刻就抛 UnicodeDecodeError。 最迷惑的地方是:命令明明跑成功了,程序却在「读输出」这一步崩掉。

三、check=True 与 capture_output=True

capture_output=True 相当于同时设 stdout=PIPE 与 stderr=PIPE, 于是你拿到一个带 stdout/stderr 属性的结果对象。

check=True 的意思是「返回码非 0 就抛异常」。 抛的是 CalledProcessError,里面带着 returncode、stdout、stderr。 好处是「忘了判返回码」这种 bug 不可能发生; 代价是你得写 try,而且要做到「异常里也能拿到 stderr」。

本机实测:子进程 exit 3,异常里 returncode 是 3,stderr 是 'boom'。 这条 stderr 就是排查的唯一线索——所以捕获异常时一定要把它写进日志。

四、超时与 kill

timeout=秒数 到点会抛 TimeoutExpired,但【子进程不会自动死】。 异常对象里带着 cmd,你要自己 kill()。

subprocess.run 在超时的时候会先 kill 再 wait, 所以用它一般不会漏掉孤儿进程; 但如果你用的是 Popen 手工管理,就必须自己收尾。

转码这种长任务,超时值按「最长可能的耗时 × 2」设, 别设成「平均耗时」,否则大文件会被误杀。

五、一份可以直接抄的模板

把前面所有要点收进一个函数:

import subprocess

def run_ffmpeg(args, timeout=3600):
    cmd = ["ffmpeg", "-hide_banner", "-nostdin"] + args
    p = subprocess.run(cmd,
                       stdin=subprocess.DEVNULL,
                       capture_output=True,
                       timeout=timeout,
                       encoding="utf-8", errors="replace")
    if p.returncode != 0:
        raise RuntimeError("ffmpeg 失败,返回码 "
                           + str(p.returncode) + ":" + p.stderr[-800:])
    return p.stdout

这十行里有五处是前面几节攒下来的:-nostdin、-hide_banner、 stdin=DEVNULL、timeout、encoding + errors。 第六处是习惯:报错只留 stderr 的最后 800 个字符。 ffmpeg 的 stderr 能有几千行,全塞进异常里,日志就没法看了。

这就是这一章的落点:调外部程序不难,难在把「默认值」一个个改对。 C++ 要自己拼引号、Java 要自己读流、Python 要把四个默认值改掉—— 三种语言,同一份清单。

六、动手 Demo:三种写法与四个坑

下面把「三种写法能拿到什么」和「编码坑」一次测完。 第三段是本机的特殊情况:这台机器被设成了 UTF-8,所以默认能通过; 而中文 Windows 的默认代码页是 GBK,同样的代码在那里会崩。

import locale, os, subprocess, sys

PY = sys.executable
print("os.system ->", os.system(PY + " -c \"print(1)\""))
r = subprocess.run([PY, "-c", "print('hi')"],
                   capture_output=True, text=True)
print("run 列表 ->", r.returncode, repr(r.stdout.strip()))

try:
    subprocess.run([PY, "-c", "import sys; sys.exit(3)"], check=True)
except subprocess.CalledProcessError as e:
    print("check=True ->", type(e).__name__, e.returncode)

print("locale ->", locale.getpreferredencoding(False))
try:
    "正在转码".encode("utf-8").decode("gbk")
except UnicodeDecodeError as e:
    print("gbk 解 utf-8 -> UnicodeDecodeError @", e.start)

实跑输出:

--------------------------------------------------------------------
12.3 Python 调用 FFmpeg 命令行
--------------------------------------------------------------------
**① 三种写法,能拿到的东西一层比一层多**

  写法                              拿到什么
  --------------------------------------------------------------------
  os.system(命令)                   只有返回值 0,输出直接打屏
  run(字符串, shell=True)           返回值 0 加输出 'hi'
  run(列表)                         返回值 0 加输出 'hi'

**第一种只适合「跑一下、成不成无所谓」;第二种能拿输出,但要多经一层 shell;**
**第三种是唯一推荐的:参数用列表,shell 这一层根本不存在。**
**而三种写法【共同】的坑是 stdin——不显式指定,子进程就继承你的。**

**② `check=True` 把「非 0」变成异常,省掉手写 if**

  写法                      子进程 exit 3 时发生什么
  ------------------------------------------------------------------------
  run(...) 默认             返回对象,returncode = 3,自己判
  run(..., check=True)      抛出 CalledProcessError,rc=3,stderr='boom'

**`check=True` 的好处是「忘了判返回码」这种 bug 再也不会发生——**
**异常里连 stderr 都给你带上了,直接就能往日志里写。**

**③ 编码:这一条在中文 Windows 上一定会撞**

  这一项                      本机的值
  --------------------------------------------------------------
  locale 的首选编码           utf-8
  拿 gbk 解 utf-8 字节        UnicodeDecodeError,位置 8

**本机这一套被设成了 UTF-8,所以默认解码能通过;**
**而中文 Windows 的默认代码页是 `cp936`(GBK),**
**ffmpeg 输出的 UTF-8 中文在那里就会撞上 `UnicodeDecodeError`。**
**这种报错最迷惑人的地方是:命令明明跑成功了,程序却在「读输出」这一步崩了。**
**所以 `text=True` 一定要配对写 `encoding="utf-8"`,再加 `errors="replace"` 兜底。**

**④ 一份可以直接抄的模板**

    import subprocess

    def run_ffmpeg(args, timeout=3600):
        cmd = ["ffmpeg", "-hide_banner", "-nostdin"] + args
        p = subprocess.run(cmd, stdin=subprocess.DEVNULL,
                           capture_output=True, timeout=timeout,
                           encoding="utf-8", errors="replace")
        if p.returncode != 0:
            raise RuntimeError("ffmpeg 失败(%d): %s"
                               % (p.returncode, p.stderr[-800:]))
        return p.stdout

**这十行里有五处是前面几节攒下来的:`-nostdin`(不吃父进程输入)、**
**`-hide_banner`(别把版本横幅混进日志)、`stdin=DEVNULL`(双保险)、**
**`timeout`(不然就可能永久卡住)、`encoding + errors`(不然中文就崩)。**
**还有一处是习惯:报错只留 stderr 的最后 800 个字符。**
**ffmpeg 的 stderr 能有几千行,全塞进异常里,日志就没法看了。**

os.system 只给 0,输出直接打到终端;run 列表能给返回码和输出。 check=True 把非 0 变成了异常,连 returncode 都带着。 最后两行:本机 locale 是 utf-8,但显式用 gbk 解就会在位置 8 崩。

30 秒实验

  1. 把 capture_output=True 换成 stdout=subprocess.PIPE:看 stderr 去哪儿了。
  2. 把 errors="replace" 删掉再看一次 gbk 解码:看异常长什么样。
  3. 给 run 加 timeout=0.01:看 TimeoutExpired 里带了什么。

常见误区

  1. 用 os.system 又想要输出。它只给状态码。
  2. text=True 不配 encoding。中文 Windows 上按 GBK 解 UTF-8,必崩。
  3. 不给 stdin。ffmpeg 会去读标准输入,后台跑时被挂起。
自测三问(点开对答案) 1. 为什么推荐传列表而不是字符串? —— 列表参数直传,不经过 shell,带空格的参数不会被切开。 2. `check=True` 的异常里有什么? —— `returncode`、`stdout`、`stderr`,够直接写日志。 3. `text=True` 在中文 Windows 上为什么容易崩? —— 默认按 locale(cp936)解码,而 ffmpeg 输出 UTF-8。

相关笔记