背景
- 之前在做视频处理相关需求时,经常遇到一些视频存在编解码的问题,需要分析视频的相关信息,因此每次都需要从点播平台找到视频地址,然后再通过本地 ffprobe 命令进行分析
- 另外很多视频在封装成标准的 hls 之前,需要转码成 mp4[lib264, acc] 格式,需要通过 ffmpeg 命令进行转码,第一次写转码工具时自己实现了一个执行命令的方法,但是感觉不是很通用。
记得之前看过一些源代码,在kafka 的源代码中有一个 shell 工具类,我觉得可以抽出来放到自己的项目里,只要稍微修改一些代码就行
下面是从 kafka 工具类中抽离出来的执行命令的工具类,包含超时执行命令处理
/**
* @author fenglh
* @version 1.0
* @description 执行命令工具
* @date 2022/10/21 14:24:07
*/
@Slf4j
public abstract class ExecuteCommandUtil {
/**
* 执行命令的名称和参数, 需要子类实现
*/
protected abstract String[] execString();
/**
* 执行结果解析,比如直接转换未字符串输出
*/
protected abstract void parseExecResult(BufferedReader lines) throws IOException;
/**
* 执行命令的超时时间
*/
private final long timeout;
/**
* 命令推出 code 码
*/
private int exitCode;
/**
* 用于执行命令的子进程
*/
private Process process;
/**
* 判断命令是否执行完成的标志
*/
private volatile AtomicBoolean completed;
/**
* @param timeout 毫秒时间,超过设置时间的命令会被杀掉进程, -1 表示不设置超时时间.
*/
public ExecuteCommandUtil(long timeout) {
this.timeout = timeout;
}
/**
* @return 进程退出的 code 码
*/
public int exitCode() {
return exitCode;
}
/**
* 获取当前执行命令的子进程
*
* @return 执行命令的进程
*/
public Process process() {
return process;
}
protected void run() throws IOException {
exitCode = 0; // reset for next run
runCommand();
}
/**
* 执行命令
*/
private void runCommand() throws IOException {
ProcessBuilder builder = new ProcessBuilder(execString());
Timer timeoutTimer = null;
// 初始化执行完成状态为未完成
completed = new AtomicBoolean(false);
process = builder.start();
if (timeout > -1) {
timeoutTimer = new Timer();
//One time scheduling.
timeoutTimer.schedule(new ShellTimeoutTimerTask(this), timeout);
}
final BufferedReader errReader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8));
final StringBuffer errMsg = new StringBuffer();
// 使用单独的线程读取错误流,释放缓冲区
Thread thread = new Thread(() -> {
try {
String line = errReader.readLine();
while ((line != null) && !Thread.currentThread().isInterrupted()) {
errMsg.append(line);
errMsg.append(System.getProperty("line.separator"));
line = errReader.readLine();
}
} catch (IOException ioe) {
log.warn("Error reading the error stream", ioe);
}
});
thread.start();
// 输出流信息交给子类处理
BufferedReader inReader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
try {
parseExecResult(inReader);
// 等待进程完成并检查退出状态码
exitCode = process.waitFor();
try {
// 确保错误流处理线程退出
thread.join();
} catch (InterruptedException ie) {
log.warn("Interrupted while reading the error stream", ie);
}
// 命令执行完成,设置标志位
completed.set(true);
//如果进程未退出,交给 finally 块中的超时线程处理
if (exitCode != 0) {
throw new ExitCodeException(exitCode, errMsg.toString());
}
} catch (InterruptedException ie) {
throw new IOException(ie.toString());
} finally {
if (timeoutTimer != null) {
timeoutTimer.cancel();
}
// 关闭输入流
try {
inReader.close();
} catch (IOException ioe) {
log.warn("Error while closing the input stream", ioe);
}
if (!completed.get())
thread.interrupt();
try {
errReader.close();
} catch (IOException ioe) {
log.warn("Error while closing the error stream", ioe);
}
// 执行完命令之后,需要关闭进程
process.destroy();
}
}
/**
* 添加了退出状态码的 IOException
*/
@SuppressWarnings("serial")
public static class ExitCodeException extends IOException {
int exitCode;
public ExitCodeException(int exitCode, String message) {
super(message);
this.exitCode = exitCode;
}
public int getExitCode() {
return exitCode;
}
}
/**
* 一个简单的shell命令执行器。
*
* 在以下情况下应使用ShellCommandExecutor
* 命令不需要显式解析,其中命令、工作目录和环境保持不变。命令的输出按原样存储,输出预计会很小。
*/
public static class ShellCommandExecutor extends ExecuteCommandUtil {
private final String[] command;
private StringBuffer output;
/**
* 创建 ShellCommandExecutor 的新实例以执行命令。
*
* @param execString 要使用参数执行的命令
* @param timeout 指定命令执行的超时时间(以毫秒为单位),-1表示不设置超时
*/
public ShellCommandExecutor(String[] execString, long timeout) {
super(timeout);
command = execString.clone();
}
/**
* 执行 shell 命令
*/
public void execute() throws IOException {
this.run();
}
protected String[] execString() {
return command;
}
/**
* 将命令执行的输出保存处理
*/
protected void parseExecResult(BufferedReader reader) throws IOException {
output = new StringBuffer();
char[] buf = new char[512];
int nRead;
while ((nRead = reader.read(buf, 0, buf.length)) > 0) {
output.append(buf, 0, nRead);
}
}
/**
* 返回命令执行输出
*/
public String output() {
return (output == null) ? "" : output.toString();
}
/**
* 返回实际命令, 带空格的参数会用引号括起来, 其他参数按原始字符展示
*
* @return 执行命令的字符串形式
*/
public String toString() {
StringBuilder builder = new StringBuilder();
String[] args = execString();
for (String s : args) {
if (s.indexOf(' ') >= 0) {
builder.append('"').append(s).append('"');
} else {
builder.append(s);
}
builder.append(' ');
}
return builder.toString();
}
}
/**
* 执行shell命令的静态方法。
* 大多数简单命令执行,不需要用户实现 ExecuteCommandUtil 接口
*
* @param cmd 执行命令
* @return 执行命令的输出
*/
public static String execCommand(String... cmd) throws IOException {
return execCommand(cmd, -1);
}
/**
* 与上面的执行命令方法类似,只不过提供了超时设置
*/
public static String execCommand(String[] cmd, long timeout) throws IOException {
ShellCommandExecutor exec = new ShellCommandExecutor(cmd, timeout);
exec.execute();
return exec.output();
}
/**
* 计时器,用于 shell 执行超时处理。
*/
private static class ShellTimeoutTimerTask extends TimerTask {
private final ExecuteCommandUtil shell;
public ShellTimeoutTimerTask(ExecuteCommandUtil shell) {
this.shell = shell;
}
@Override
public void run() {
Process p = shell.process();
try {
p.exitValue();
} catch (Exception e) {
//如果进程还未结束执行,直接结束进程处理
if (p != null && !shell.completed.get()) {
p.destroy();
}
}
}
}
}
有了这个工具类之后,实现在线分析视频信息的接口就非常简单了,这里我写了一个简单的测试 controller, 来测试我们的 ffprobe 命令
@RequestMapping("v1/video/check/with/command")
public String getVideoDetailsByCommand(String command, String mediaId) throws Exception {
GetMezzanineInfoResponse response = aliVideoService.getMezzanineInfo(mediaId);
if (null == response || null == response.getMezzanine()) {
return "未获取到源文件";
}
String ffrobeCmd = String.format(command, response.getMezzanine().getFileURL());
return ExecuteCommandUtil.execCommand(ffrobeCmd.split(" "));
}
总结
其实有的时候,不一定什么代码都要从零开始的,如果有看过的开源代码可以借鉴,拿过来直接改一改可以用是,最好最快的方法,因为好的开源代码往往都经过千锤百炼的,而且这样的代码 bug 也比较少