一、写在前面
最近项目上有这么一个需求:
利用ffmpeg将用户上传的视频(MP4)转换为.ts文件后切片,再上传到minIO上。
因为之前都没使用过ffmpeg和minIO,所以哼哧哼哧百度了代码复制过来实现这个需求。
自测的时候,都是使用一些小的视频文件(没有超过10M的),所以功能很正常。进入到测试阶段后,发现稍微大点的文件(100M以上)就挂了。
查看日志,发现进入到java调用shell脚本,去进行.ts文件切片时就卡住了,等待一段时间后,前端就报错了。
于是乎,展开了这个问题的“漫长”调查之旅。
二、问题分析及排查
分析前,先将ffmpeg处理MP4的shell脚本里的命令分享一下
#!/bin/bash
# 执行两次命令,比一次命令效率高
# 转换为ts文件
ffmpeg -i $1.mp4 -c copy -bsf:v h264_mp4toannexb $1.ts
# 将ts切片
ffmpeg -i $1.ts -c copy -map 0 -f segment -segment_time 30 -segment_list $1.m3u8 $1_%05d.ts
# 删除mp4文件
rm -f $1.mp4
# 删除ts文件
rm -f $1.ts
参数说明:
$1:这个参数是java调用shell脚本时传进来的参数,是MP4在服务器上的路径。
其他ffmpeg的参数说明,可以去百度查找,这里就不做分享了。
1、首先怀疑的是切片命令是否有问题
之前的命令为:ffmpeg -i $1.ts -c copy -map 0 -f segment -segment_time 30 -segment_list $1.m3u8 $1_%05d.ts
修改后命令为:ffmpeg -i $1.ts -c copy -map 0 -f segment -segment_list $1.m3u8 -segment_time 30 $1_%05d.ts
区别就是将【-segment_list $1.m3u8】与【-segment_time 30】调换了位置,可问题未得到解决。
2、怀疑java调用shell脚本的代码是否有问题
照例,先将之前的代码分享一下
/**
* 运行shell并获得结果,注意:如果sh中含有awk,一定要按new String[]{"/bin/sh","-c",shStr}写,才可以获得流
*
* @param shStr 需要执行的shell
* @return
*/
public List<String> runShell(String shStr) throws Exception {
log.info("RemoteShellUtil runShell start");
log.info("RemoteShellUtil runShell shStr=" + shStr);
Process process = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", shStr}, null, null);
process.waitFor();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
List<String> result = new ArrayList<String>();
String line;
while ((line = br.readLine()) != null) {
result.add(line);
}
log.info("RemoteShellUtil runShell end");
return result;
}
参数说明:
shStr:需要调用shell的命令及参数拼接字符串,比如【". ./videoFfmpegFormat.sh " + fullPath】,其中【fullPath】就是shell脚本中【$1】
修改后的代码
/**
* 执行shell脚本
*
* @author: caip
* @date: 2021-05-23 09:11:40
* @param shellPath shell脚本路径
* @param params 参数数组
* @return
*/
public List<String> runShell(String shellPath, String... params) {
log.info("RemoteShellUtil runShell start");
List<String> strList = new ArrayList<String>();
ProcessBuilder pb = new ProcessBuilder(params);
pb.directory(new File(shellPath));
String s = null;
try {
Process p = pb.start();
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(p.getErrorStream()));
while ((s = stdInput.readLine()) != null) {
log.error(s);
strList.add(s);
}
while ((s = stdError.readLine()) != null) {
log.error(s);
strList.add(s);
}
this.dealStream(p);
p.waitFor();
} catch (Exception e) {
String msg = "shell脚本执行错误!" + e.getMessage();
log.error(msg, e);
strList.add(msg);
}
log.info("RemoteShellUtil runShell end");
return strList;
}
参数说明:
shellPath:shell脚本路径;
params:参数数组。这里包括了shell文件名,参数,比如:
String[] params = new String[] {"./videoFfmpegFormat.sh", fullPath};
虽然上面的代码,相比于最先的代码有了很大的修改,但其实问题仍然没有得到解决。
3、最终解决方案
/**
* 执行shell脚本
*
* @author: caip
* @date: 2021-05-23 09:11:40
* @param shellPath shell脚本路径
* @param params 参数数组
* @return
*/
public List<String> runShell(String shellPath, String... params) {
log.info("RemoteShellUtil runShell start");
List<String> strList = new ArrayList<String>();
ProcessBuilder pb = new ProcessBuilder(params);
// 这是重中之重
pb.redirectErrorStream(true);
pb.directory(new File(shellPath));
String s = null;
try {
Process p = pb.start();
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
while ((s = stdInput.readLine()) != null) {
log.info(s);
strList.add(s);
}
// this.dealStream(p);
p.waitFor();
} catch (Exception e) {
String msg = "shell脚本执行错误!" + e.getMessage();
log.error(msg, e);
strList.add(msg);
}
log.info("RemoteShellUtil runShell end");
return strList;
}
上面代码里的【pb.redirectErrorStream(true);】是解决此篇文章问题的关键,这个方法的解释是这样的:
告诉此进程生成器是否合并标准错误和标准输出。如果此属性为true,则通过子进程所产生的任何错误输出随后由该对象的start()方法启动将与标准输出合并,这样既可以用Process.getInputStream()方法来读取。此使得更容易与对应的输出相关的错误消息。初始值是false。
意思就是,如果设置为true,那将在执行shell脚本时,无论正确的消息,还是错误的消息,都一起通过【p.getInputStream()】返回。
那么,一开始使用ffmpeg切片ts文件卡住(其实是阻塞)的问题,其实就是遇到错误消息时线程阻塞的问题。阻塞后,无法得到正常的返回(其实查看系统日志可以看到错误消息),才导致莫名其妙的不了了之。