Java 里有两个入口:Runtime.exec 与 ProcessBuilder。 前者会把字符串按空格切开,引号完全失效;所以只用后者。
C++ 那五节讲的三件事(传参、等结束、收输出),Java 里一件没少。 好处是标准库把管道包好了,坏处是它包得不够彻底——流还是要你自己读。
一、两个入口,只该用后面那个
Runtime.exec 有两个重载:一个收字符串,一个收字符串数组。
收字符串那个会用 StringTokenizer 按空白切开,
它不认识引号——"a b.mp4" 会被切成两段。
ProcessBuilder 只收 List<String>,从接口上就杜绝了这个误会。
附带的好处是能设工作目录、能改环境变量、能合并两路输出。
所以结论很干脆:新代码一律用 ProcessBuilder。
老代码里如果见到 Runtime.exec("ffmpeg -i ..."),
而路径里恰好没有空格,它就跑得好好的——
这种「碰巧能跑」的代码最危险:换个目录名就崩,
而崩溃现象是「找不到文件」,跟引号八竿子打不着。
二、三处必须自己动手的地方
标准库把「起进程」做完了,剩下三件事还是你的:
要做的事 不做会怎样
读完 stdout 与 stderr 缓冲满,子进程阻塞 给 waitFor 加超时 无限等,线程被占死 等结束再取 exitValue 抛 IllegalThreadStateException
第一条是最容易漏的:Process 的流必须读到 EOF。
只 waitFor() 不读,缓冲区一满子进程就写不动,
两边互等——跟 Qt、跟 C++ 是同一个死锁。
本机实测那个临界点还是 4 KB 到 4.5 KB 之间,跟语言无关。
三、redirectErrorStream 是最省事的一个开关
pb.redirectErrorStream(true) 把 stderr 并进 stdout。
之后只读一个流就够了,死锁风险直接消失一半。
这个开关在三个语言里各有一个名字:Java 叫 redirectErrorStream,
Qt 叫 MergedChannels,Python 叫 stderr=STDOUT。
它们做的事一模一样——把两根管子拧成一根。
代价也一样:两路的先后顺序不再可靠。
如果确实需要分开,那就要么开两个线程各读一路,
要么用 ProcessBuilder.Redirect 把某一路重定向到文件。
两者都比「先等后读」安全。
四、超时与强制结束
p.waitFor(10, TimeUnit.SECONDS) 返回布尔值:
true 表示在超时前结束了,false 表示还没结束。
返回 false 之后必须自己处理——destroy() 是优雅终止,destroyForcibly() 是强杀。
顺序跟 Qt 那节一样:先 destroy(),等一会儿,还在就 destroyForcibly()。
ffmpeg 被强杀时 mp4 的尾写不完,文件是坏的;
优雅终止它会尽量写完索引再退出。
五、一份能用的 Java 骨架
把上面几件事拼起来:
List<String> cmd = Arrays.asList("ffmpeg", "-y", "-nostdin", "-i", in);
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true); // 两路合一
Process p = pb.start();
StringBuilder log = new StringBuilder();
try (BufferedReader r = new BufferedReader(
new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) log.append(line).append('\n');
} // try-with-resources 保证读到底
if (!p.waitFor(30, TimeUnit.MINUTES)) {
p.destroy();
if (!p.waitFor(5, TimeUnit.SECONDS)) p.destroyForcibly();
throw new RuntimeException("ffmpeg 超时");
}
if (p.exitValue() != 0)
throw new RuntimeException("ffmpeg 失败: " + tail(log.toString(), 800));
三处细节:-nostdin 写在参数里(防它去读标准输入)、
StandardCharsets.UTF_8 显式指定编码(不然中文日志会乱)、
try-with-resources 保证流读到 EOF(这是防死锁的关键结构)。
六、动手 Demo:三个坑,逐个复现
Java 本机没装 JDK,所以下面用 Python 复现同一层的三个现象。 它们跟语言无关:切参数、管道满、流不读,都是操作系统那一层的事。
import subprocess, sys
PY = sys.executable
args = ["a b.mp4", "-c:v", "libx264"]
print("数组传:", len(args), "个")
print("按空格切:", len(" ".join(args).split()), "个")
for n in (4096, 4608):
p = subprocess.Popen(
[PY, "-c", "import sys; sys.stdout.write('x'*%d)" % n],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
p.wait(timeout=1.5)
print("%d 字节: 出来了" % n)
except subprocess.TimeoutExpired:
print("%d 字节: 卡住" % n)
p.kill(); p.wait()
实跑输出:
--------------------------------------------------------------------
12.2 Java 调用 FFmpeg 命令行
--------------------------------------------------------------------
**① `Runtime.exec(String)` 会按空格切,引号不算数**
**Java 里 `Runtime.exec("ffmpeg -i a b.mp4")` 不会把整串交给系统,**
**它自己先用 `StringTokenizer` 切开——所以带空格的参数在 Java 里就散了。**
**下面的实验用「按空格切」复现它的行为,再跟「按数组传」对照:**
怎么传 切出几个参数
------------------------------------------------------
按数组传(ProcessBuilder 的写法) 3 个
按空格切(Runtime.exec 的做法) 4 个
**3 个变 4 个,`a b.mp4` 被劈成两半——这就是 Java 里那句著名的**
**「`Runtime.exec` 不支持引号」的实测版。**
**结论很干脆:一律用 `new ProcessBuilder(List<String>)`,别用字符串重载。**
**② 起了进程不读流,`waitFor()` 会把你吊死**
子进程往 stdout 写 只调 waitFor 的结果
----------------------------------------------------
4096 字节 正常返回
4608 字节 卡住
65536 字节 卡住
**跟 Qt 那节测出来的是同一个数:4 KB 到 4.5 KB 之间就满了。**
**因为卡人的不是 Qt、也不是 Java,是操作系统的管道。**
**语言换了三层,卡的位置一个字节都没变——这也是这一章最想让你看见的事:**
**坑在系统那一层,不在语法那一层。**
**③ 合并两路是最省事的解法**
怎么收 收到的顺序
----------------------------------------------------------------
两路分开收 stdout=O0O1O2 stderr=E0E1E2
合并成一路收 E0O0E1O1E2O2
**合并之后时间是连续的,两路分开收则是「先 E0E1E2 再 O0O1O2」——**
**因为你读完一路才去读另一路。**
**Java 里对应的是一个布尔参数:`builder.redirectErrorStream(true)`。**
**它就是 Qt 那个 `MergedChannels`,也是 Python 的 `stderr=STDOUT`——**
**三个语言,同一个开关。**
**④ Java 骨架(本机没有 JDK,这段是写给你抄的)**
List<String> cmd = Arrays.asList("ffmpeg", "-y", "-i", "a b.mp4",
"-c:v", "libx264", "out.mp4");
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true); // 两路合一,最省事
Process p = pb.start();
StringBuilder log = new StringBuilder();
try (BufferedReader r = new BufferedReader(
new InputStreamReader(p.getInputStream(), "UTF-8"))) {
String line;
while ((line = r.readLine()) != null) log.append(line).append('\n');
}
if (!p.waitFor(10, TimeUnit.SECONDS)) p.destroyForcibly();
int rc = p.exitValue();
**这段里三处关键:`Arrays.asList` 传数组(引号问题消失)、**
**`redirectErrorStream(true)`(死锁问题消失)、**
**`waitFor(10, TimeUnit.SECONDS)`(无限期等待消失)。**
**而 `exitValue()` 必须在 `waitFor` 之后调,否则抛 `IllegalThreadStateException`——**
**它只肯在你「已经确定进程结束了」之后才回答。**
第一段 3 个变 4 个,就是 Runtime.exec(String) 干的事。
第二段 4096 出得去、4608 卡住,就是「不读流就等」的下场。
换成 Java、换成 Qt,这两个数字都不会变。
30 秒实验
- 把
redirectErrorStream(true)去掉,只读 stdout:看 stderr 藏到哪儿去了。 - 把
waitFor的超时改成 1 毫秒:看返回 false 之后exitValue()会抛什么。 - 把
-nostdin删掉:在后台跑一次,看进程会不会挂起。
常见误区
- 用
Runtime.exec("ffmpeg -i a b.mp4")。字符串会被按空格切开,引号无效。 - 只
waitFor()不读流。缓冲区满就永久阻塞。 - 在
waitFor之前调exitValue()。抛IllegalThreadStateException。
自测三问(点开对答案)
1. 为什么必须用 `ProcessBuilder` 而不是 `Runtime.exec(String)`? —— 后者按空白切分字符串,引号不生效,带空格的参数会被拆开。 2. `redirectErrorStream(true)` 的代价是什么? —— 两路合并后先后顺序不再可靠。 3. `waitFor` 返回 false 说明什么? —— 超时了,进程还在跑;此时不能调 `exitValue()`。- ☐ 12.2 完——下一节 12.3 Python 调用 FFmpeg 命令行