从代码里调 ffmpeg,只有三件事要做对:参数怎么传、进程怎么等、输出怎么收。 三件事里唯一随平台变的是第一件——Windows 只收一条字符串。
前面十一章都是「人敲命令」,从这一章起换成「程序敲命令」。 区别只有一点:人知道哪块是文件名,程序只看见一串参数。 这一节先立骨架,后面五节逐个平台拆。
一、人敲命令与程序敲命令,差的是一层「谁负责切分」
在命令行窗口里敲 ffmpeg -i a.mp4 out.mp4,切分这件事是 shell 干的。
它看见空格就切一刀,把四段交给 ffmpeg。
程序里没有 shell 这一层(除非你主动要)。 于是「谁按什么规则把这一行切成若干段」这个问题,就落到了你身上。 这一层看似琐碎,但它是后面所有「路径带空格就报错」的总根。
换个角度看:调用 ffmpeg 这件事,拆开只有三件事。
要做的事 一句话说清
传参数 把若干段字符串交给系统,别让它乱切 等结束 知道它什么时候停、停得好不好 收输出 把 stdout 与 stderr 两路都拿走
三件事在任何语言里都成立。C++ 用 execv 与管道,Java 用 ProcessBuilder,
Python 用 subprocess——名字不同,做的还是这三件。
所以这一章真正要记的不是 API,是这三件事各自的坑。
二、参数:数组与字符串,是两种东西
Linux 与 macOS 的内核接口收的是【数组】:
execv(path, argv) 里的 argv 是一个 char* 数组,每个元素就是一个完整参数。
你传什么,ffmpeg 就收到什么,中间没有任何切分动作——
带空格的路径在这条路上天然安全。
Windows 的内核接口收的是【一条字符串】:
CreateProcess 只给一个 lpCommandLine,没有数组可选。
系统在对面那头按一套固定规则把它切回数组。
于是「哪几个字属于同一个参数」这件事,只能靠在字符串里加引号表达出来。
这套规则不复杂,但反直觉的地方不少:
你在字符串里写 对面收到 为什么
"a b.mp4" a b.mp4 引号把空格包住了 a b.mp4 a、b.mp4 两段 引号是唯一的分组手段 "C:\my dir\" C:\my dir\ 结尾反斜杠要翻倍 a"b 报错或截断 裸引号必须转义成 \"
第三行是最容易写错的一处:路径以反斜杠结尾(目录),又必须加引号时, 那个反斜杠要写成两个。不写两个,它会跟后面的收尾引号拼成「转义引号」, 整个参数连着后面几段一起被吞掉。 报错信息通常指向一个莫名其妙的参数,让人完全想不到是路径结尾惹的。
三、退出码:只有「成」与「不成」两档
ffmpeg 的返回码约定很简单:0 成功,非 0 失败。 它不会用 1 表示「参数错」、2 表示「文件不存在」—— 具体哪里错了,全在 stderr 那几千行里。
所以一个封装函数的返回值应该是「成败+一段 stderr」,而不是「一个数字」。 把非 0 直接当错误码打印出来的代码,用户看到的是「错误码 1」, 而你看到的是「还得去翻日志」,两边都没得到有用信息。
这里还有一层平台差异要注意:Linux 的 system() 与 pclose() 返回的不是退出码,
是「等待状态」——一个 16 位的打包值,高 8 位才是退出码。
Windows 上同一个函数直接返回退出码。
同一行 if (rc != 0) 两边都对,但一旦你要打印这个数,两边就差 256 倍。
四、输出:两路都要读,而且要在等之前读
子进程的输出走两根管子:stdout 与 stderr。 ffmpeg 的规矩是——媒体数据走 stdout,日志与进度走 stderr。
很多人只接 stdout,结果 ffmpeg 的报错一行都收不到;
更多人两路都接了,却先 wait() 再读——
这时候如果子进程已经写满管子,它写不进去就停在那儿,
而你在等它退出,两边互等,程序就永久卡住了。
这条管子的容量小得可怜,本机实测4 KB 到 4.5 KB 之间就满了。 ffmpeg 打十几行进度就能填满它。 所以正确顺序永远是:边读边等,或者干脆把两路合并成一路再一次读完。
五、一个能用的 C++ 封装,长什么样
把上面四件事拼起来,一个够用的封装大概是这样(Windows 版):
bool runFFmpeg(const std::vector<std::string>& args,
std::string& outLog) {
std::string line = list2cmdline(args); // 自己拼引号,规则见 12.1.2
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE };
HANDLE rd = nullptr, wr = nullptr;
CreatePipe(&rd, &wr, &sa, 0); // 只接 stdout+stderr 合并
STARTUPINFOW si{}; si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = wr; si.hStdError = wr;
PROCESS_INFORMATION pi{};
std::wstring w = widen(line);
if (!CreateProcessW(nullptr, &w[0], nullptr, nullptr, TRUE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi))
return false;
CloseHandle(wr);
char buf[4096]; DWORD n = 0;
while (ReadFile(rd, buf, sizeof(buf), &n, nullptr) && n)
outLog.append(buf, n); // 边读边等,绝不先 wait
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD rc = 0; GetExitCodeProcess(pi.hProcess, &rc);
CloseHandle(rd); CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
return rc == 0; // 只判成败,具体原因看 outLog
}
这段里每一行都对应前面讲到的一个坑:list2cmdline 是引号,
hStdError = wr 是两路合并,while (ReadFile…) 是边读边等,
CREATE_NO_WINDOW 是不弹黑框,最后 return rc == 0 是只判成败。
Linux 版把中间那段换成 fork + execv + pipe,逻辑一模一样,
只是引号那一步可以整段删掉——因为数组天然不需要引号。
这就是「跨平台通用代码」的全部含义:
同一套流程,只在「拼串」这一步分叉。
六、动手 Demo:参数怎么传,才不会被拆开
下面用一个只报告「收到几个参数」的小子程序,当作 ffmpeg 的替身。 同一组参数,一种按数组传、一种拼成字符串交给 shell,看差别。
import subprocess, sys
PY = sys.executable
CH = "import sys; print(len(sys.argv) - 1, sys.argv[1:])"
args = ["a b.mp4", "-c:v", "libx264"]
r1 = subprocess.run([PY, "-c", CH] + args,
capture_output=True, text=True)
print("列表传:", r1.stdout.strip())
cmd = PY + " -c \"" + CH + "\" " + " ".join(args)
r2 = subprocess.run(cmd, shell=True, capture_output=True, text=True)
print("字符串传:", r2.stdout.strip())
实跑输出:
--------------------------------------------------------------------
12.1.1 C++ 调用 FFmpeg 命令行的跨平台通用代码
--------------------------------------------------------------------
**① 同一组参数,两种传法,子进程收到的完全不是一回事**
**先在代码里写一段「把收到的参数原样打印出来」的子程序,当作 ffmpeg 的替身:**
**它只干一件事——报告自己收到几个参数、分别是什么。**
**后面几节反复用它,因为「参数有没有被拆开」是这一章所有坑的总根。**
传法 参数个数 子进程实际收到
-----------------------------------------------------------------------------------------
列表逐项传(execv 语义) 4 ['a b.mp4', '-c:v', 'libx264', '-y']
拼成一整条字符串(shell) 5 ['a', 'b.mp4', '-c:v', 'libx264', '-y']
**同一组参数,左边是 4 个,右边变成 5 个——**
**`a b.mp4` 在空格处被切成了 `a` 和 `b.mp4`,多出来的那一个就是这么来的。**
**ffmpeg 收到这种参数,报的是「找不到 a」或者「无法识别 b.mp4」,**
**而你回头去看命令行,明明写着完整的文件名。**
**这就是「跨平台通用代码」要解决的第一件事:**
**Linux/macOS 上 `execv` 直接收数组,空格进不来;**
**Windows 的 `CreateProcess` 只收【一条字符串】,必须自己把数组拼回去、该加引号的加引号。**
**② 三平台各自要做的三件事,其实只有「拼串」这一栏不同**
要做的事 Linux/macOS Windows
--------------------------------------------------------------------------------
把参数交给系统 execv:数组直接传 CreateProcess:一条命令行字符串
引号谁负责 系统不管,数组天然隔离 调用者负责,规则是 MSVC 那套
等它结束 waitpid WaitForSingleObject
取退出码 WEXITSTATUS(status) GetExitCodeProcess
**「引号谁负责」这一栏就是分水岭。**
**在 Linux 上你根本不需要想引号——`{"a b.mp4"}` 传下去就是一个参数;**
**在 Windows 上你不拼引号,系统就只能在空格处下刀。**
**③ 退出码只有两档,别指望用数字区分错误**
子程序怎么结束的 退出码 它往 stderr 说了什么
------------------------------------------------------------------------
正常退出 0 —
参数错误(模拟) 3 Invalid argument
**真正的 ffmpeg 只约定了一条:0 是成功,非 0 是失败。**
**它不会用 1、2、3 区分「参数错」和「文件不存在」——**
**要区分,只能去读 stderr 里那句话。**
**所以封装函数返回值只有「成败」两个状态,附带一段 stderr 文本,这才是对的接口。**
**④ 通用封装就四步,每一步对应一个坑**
步骤 要做什么 不做的后果
-------------------------------------------------------------------------------
拼参数 vector<string> → 一条命令行 含空格的路径被切开
起进程 execv/CreateProcess 只能退回 system(),多一层 shell
边读边等 两路输出同时收 输出一多就永久卡死
解返回 0 与非 0 两分 把 768 当成错误码打印
**这四步在任何语言里都是同一套,后面的 C++/Java/Python 三节,**
**无非是这四步换了个说法。**
两行的参数个数不一样:列表传是 3 个,字符串传变成 4 个。
多出来的那一个,就是 a b.mp4 在空格处被切开的结果。
在 Linux 上这段代码的差别更彻底:execv 直接收数组,
根本没有「切」这个动作,所以带空格的路径从来不是问题。
30 秒实验
- 把参数换成
C:\my dir\a.mp4:看列表传能不能保住反斜杠与空格。 - 把子程序的退出码改成 3:看
returncode拿到 3 还是 768。 - 把
capture_output=True删掉:看输出跑到哪里去了。
常见误区
- 以为「加了引号就安全」。引号只在字符串传法里有用,列表传法里引号会变成参数内容。
- 只读 stdout。ffmpeg 的错误与进度全在 stderr,只读一路等于没有日志。
- 先
wait()再读。管道 4 KB 就满,输出一多就永久卡死。
自测三问(点开对答案)
1. 为什么 Windows 上必须有「拼引号」这一步? —— 因为 CreateProcess 只收一条字符串,参数边界只能靠引号表达。 2. ffmpeg 的退出码能区分错误类型吗? —— 不能。只有 0 与非 0,具体原因在 stderr 里。 3. Linux 上 `system()` 返回 256 说明什么? —— 子进程退出码是 1,`>> 8` 之后才是 1。- ☐ 12.1.1 完——下一节 12.1.2 Visual Studio 调用 FFmpeg 命令行