Java调用FFmpeg进行视频处理及Builder设计模式的应用


1、FFmpeg是什么

FFmpeg(https://www.ffmpeg.org)是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它用来干吗呢?视频采集、视频格式转化、视频截图、视频添加水印、视频切片(m3u8、ts)、视频录制、视频推流、更改音视频参数(编码方式、分辨率、码率、比特率等)功能,等等...

下载下来解压完了呢是这个样子:

bin中文件夹有个 ffmpeg.exe,点开,是的,一闪而逝并没有什么用,因为ffmpeg靠命令行来调用:

如上图命令读取 《小星星》.mp4 文件的文件信息,下面的一大片输出就是文件的信息输出了。

2、Java调用FFmpeg

现在我们需要把目标视频在第10秒处截图,怎么做呢,先看看用命令行是这样:

C:\workspace\project\greejoy\picManager\web\tools\ffmpeg\bin\ffmpeg.exe -i C:\Users\Dulk\Desktop\ukulele\01\《小星星》.mp4 -f image2 -ss 10 -t 0.001 -s 320*240 C:\Users\Dulk\Desktop\ukulele\01\littleStar.jpg
其中:
  • C:\workspace\project\greejoy\picManager\web\tools\ffmpeg\bin\ffmpeg.exe 指 ffmpeg.exe 的路径
  • C:\Users\Dulk\Desktop\ukulele\01\《小星星》.mp4 指源视频路径
  • C:\Users\Dulk\Desktop\ukulele\01\littleStar.jpg 指截图的输出路径
  • -i 表示输入文件的命令
  • -f 表示输出格式,image2表示输出为图片
  • -ss 表示指定的起始位置,这里设置为10,即10s处
  • -t 表示设置录制/转码时长,既然截图就0.001就足够了
  • -s 表示size,设置帧大小

现在我们看看用Java调用FFmpeg来实现相同的目的,要用到 ProcessBuilder 这个类,该类用于创建操作系统进程,执行本地命令或脚本等工作,其属性command是一个字符串列表(用以输入命令),然后通过start()方法启动执行,但需要注意的是,该方法调用会启动新的进程执行操作,所以并不是和原Java代码同步执行的

public class FFmpegTest {

    public static void main(String[] args) {

        String ffmpegExePath = "C:\\workspace\\project\\greejoy\\picManager\\web\\tools\\ffmpeg\\bin\\ffmpeg.exe";
        String inputFilePath = "C:\\Users\\Dulk\\Desktop\\ukulele\\01\\《小星星》.mp4";
        String outputFilePath = "C:\\Users\\Dulk\\Desktop\\ukulele\\01\\littleStarJava.jpg";

        List<String> command = new ArrayList<String>();
        command.add(ffmpegExePath);
        command.add("-i");
        command.add(inputFilePath);
        command.add("-f");
        command.add("image2");
        command.add("-ss");
        command.add("10");
        command.add("-t");
        command.add("0.001");
        command.add("-s");
        command.add("320*240");
        command.add(outputFilePath);

        ProcessBuilder builder = new ProcessBuilder();
        builder.command(command);
        //正常信息和错误信息合并输出
        builder.redirectErrorStream(true);
        try {
            //开始执行命令
            Process process = builder.start();

            //如果你想获取到执行完后的信息,那么下面的代码也是需要的
            StringBuffer sbf = new StringBuffer();
            String line = null;
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            while ((line = br.readLine()) != null) {
                sbf.append(line);
                sbf.append(" ");
            }
            String resultInfo = sbf.toString();
            System.out.println(resultInfo);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

可以看到,拼接命令实际上就是把参数和值按序传入一个List,然后将该List传给 ProcessBuilder 类,然后执行 start() 方法即可。可以看到控制台输出的信息如下:

需要注意的是:
  • 操作执行信息的读取是分为正常信息和错误信息,分别需要使用 getInputStream() 和 getErrorStream() 读取,上例使用了 builder.redirectErrorStream(true); 将两者合并输出
  • 如果要使用 Process.waitFor() 则需要小心阻塞问题,此处不展开(FFmpeg在JAVA中的使用以及Process.waitFor()引发的阻塞问题

3、Builder设计模式的应用

如果你每次使用都要如上例中add()一大堆参数,我估计也是头疼:
List<String> command = new ArrayList<String>();
command.add(ffmpegExePath);
command.add("-i");
command.add(inputFilePath);
command.add("-f");
command.add("image2");
command.add("-ss");
command.add("10");
command.add("-t");
command.add("0.001");
command.add("-s");
command.add("320*240");
command.add(outputFilePath);

后来一想,这简直就是绝佳的使用builder模式的示例,先把命令封装成一个类:
/**
 * FFmpeg命令的封装类
 */
public class FFmpegCommand {

    private List<String> command;

    public FFmpegCommand(List<String> command) {
        this.command = command == null ? new ArrayList<String>() : command;
    }

    public List<String> getCommand() {
        return command;
    }

    public void setCommand(List<String> command) {
        this.command = command;
    }

    /**
     * 开始执行命令
     *
     * @param callback 回调
     * @return 命令的信息输出
     * @throws FFmpegCommandException
     */
    public String start(FFmpegCallback callback) throws FFmpegCommandException {
        BufferedReader br = null;
        StringBuffer sbf = new StringBuffer();
        String resultInfo = null;
        try {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command(command);
            //正常信息和错误信息合并输出
            builder.redirectErrorStream(true);
            //开启执行子线程
            Process process = builder.start();

            String line = null;
            br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            while ((line = br.readLine()) != null) {
                sbf.append(line);
                sbf.append(" ");
            }
            resultInfo = sbf.toString();

            //等待命令子线程执行完成
            int exitValue = process.waitFor();
            //完成后执行回调
            if (exitValue == 0 && callback != null) {
                callback.complete(resultInfo);
            }
            //销毁子线程
            process.destroy();

        } catch (IOException e) {
            e.printStackTrace();
            throw new FFmpegCommandException(e.getMessage());
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new FFmpegCommandException("线程阻塞异常:" + e.getMessage());
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultInfo;
    }

}

自然,要有builder类:
public class FFmpegCommandBuilder {

    List<String> command = new ArrayList<String>();

    public FFmpegCommandBuilder(String exePath) {
        if (exePath == null) {
            throw new FFmpegCommandRuntimeException("ffmpeg.exe 路径不得为空");
        }
        //添加命令的exe执行文件位置
        command.add(exePath);
    }

    /**
     * 添加输入文件的路径
     *
     * @param inputFilePath
     */
    public FFmpegCommandBuilder input(String inputFilePath) {
        if (inputFilePath != null) {
            command.add("-i");
            command.add(inputFilePath);
        }
        return this;
    }

    /**
     * 添加输出文件的路径
     *
     * @param outputFilePath
     */
    public FFmpegCommandBuilder output(String outputFilePath) {
        if (outputFilePath != null) {
            command.add(outputFilePath);
        }
        return this;
    }

    /**
     * 覆盖输出文件
     */
    public FFmpegCommandBuilder override() {
        command.add("-y");
        return this;
    }

    /**
     * 强制输出格式
     *
     * @param format 输出格式
     */
    public FFmpegCommandBuilder format(FFmpegCommandFormatEnum format) {
        if (format != null) {
            command.add("-f");
            command.add(format.getValue());
        }
        return this;
    }

    /**
     * 设置录制/转码的时长
     *
     * @param duration 形如 0.001 表示0.001秒,hh:mm:ss[.xxx]格式的记录时间也支持
     */
    public FFmpegCommandBuilder duration(String duration) {
        if (duration != null) {
            command.add("-t");
            command.add(duration);
        }
        return this;
    }

    /**
     * 搜索到指定的起始时间
     *
     * @param position 形如 17 表示17秒,[-]hh:mm:ss[.xxx]的格式也支持
     */
    public FFmpegCommandBuilder position(String position) {
        if (position != null) {
            command.add("-ss");
            command.add(position);
        }
        return this;
    }

    /**
     * 设置帧大小
     *
     * @param size 形如 xxx*xxx
     * @return
     */
    public FFmpegCommandBuilder size(String size) {
        if (size != null) {
            command.add("-s");
            command.add(size);
        }
        return this;
    }

    /**
     * 创建FFmpegCommand命令封装类
     *
     * @return FFmpegCommand
     */
    public FFmpegCommand build() {
        return new FFmpegCommand(command);
    }

}

那么使用builder模式,还是刚才的目的,我们的代码就变成了:
public class FFmpegBuilderTest {

    public static void main(String[] args) {

        String ffmpegExePath = "C:\\workspace\\project\\greejoy\\picManager\\web\\tools\\ffmpeg\\bin\\ffmpeg.exe";
        String inputFilePath = "C:\\Users\\Dulk\\Desktop\\ukulele\\01\\《小星星》.mp4";
        String outputFilePath = "C:\\Users\\Dulk\\Desktop\\ukulele\\01\\littleStarJavaBuilder.jpg";

        FFmpegCommandBuilder builder = new FFmpegCommandBuilder(ffmpegExePath);
        builder.input(inputFilePath).format(FFmpegCommandFormatEnum.IMAGE)
               .position("10").duration("0.001").size("320*240").output(outputFilePath);
        FFmpegCommand command = builder.build();
        try {
            String result = command.start(null);
            System.out.println(result);
        } catch (FFmpegCommandException e) {
            e.printStackTrace();
        }
    }

}

posted @ 2018-12-15 13:20  Dulk  阅读(12559)  评论(1编辑  收藏  举报