java版ffmpeg 照片合成视频

891 阅读4分钟

最近做的一个照片合成视频的需求,记下笔记。

1. JAVACV 照片合成视频

maven依赖

<!-- 全量包-->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.7</version>
</dependency>

精简版

<!-- ffmpeg最小依赖 -->
<dependency>
   <groupId>org.bytedeco</groupId>
   <artifactId>javacv</artifactId>
   <version>1.5.7</version>
</dependency>
<dependency>
   <groupId>org.bytedeco</groupId>
   <artifactId>javacpp-platform</artifactId>
   <version>1.5.7</version>
</dependency>
<!-- ffmpeg最小依赖 -->
<dependency>
   <groupId>org.bytedeco</groupId>
   <artifactId>ffmpeg</artifactId>
   <version>5.0-1.5.7</version>
   <classifier>windows-x86_64</classifier>
</dependency>
<dependency>
   <groupId>org.bytedeco</groupId>
   <artifactId>ffmpeg</artifactId>
   <version>5.0-1.5.7</version>
   <classifier>linux-x86_64</classifier>
</dependency>

由于全量依赖体积过大,所以做精简化依赖

public class VideoConstant {
    /**
     * 视频长
     */
    public static final int VIDEO_WIDTH=1600;
    /**
     * 视频宽
     */
    public static final int VIDEO_HEIGHT=900;
    /**
     * 帧数
     */
    public static final int VIDEO_FRAME=12;
    /**
     * 张数
     */
    public static final int LIMIT=120;

    /**
     * 视频临时存放位置(linux)
     */
    public static final String VIDEO_PATH="/home/video";

}
/**
 * 合成视频
 */
@PostMapping("/compose")
public AjaxResult compose(@Validated @RequestBody MessageBody messageBody) throws MalformedURLException {

    try {
        String mp4SavePath = VideoConstant.VIDEO_PATH+File.separator+ IdUtil.fastSimpleUUID() +".mp4";
        //根据mapper查询结果集
        //List<String> urls=iCameraFileTimerService.selectTimerByRoute(messageBody.getRoute(),VideoConstant.LIMIT);
        List<String> urls=new ArrayList<>();
        Map<Integer, URL> imgMap = new HashMap<>();
        for (int i = 0; i < urls.size(); i++) {
            imgMap.put(i,new URL(urls.get(i)));
        }
        if (imgMap.isEmpty()) {
            return error("该模板维护中,无法生成视频");
        }
        boolean flag=createMp4(mp4SavePath, imgMap);
        if (flag) return success(url);
    } catch (IOException e) {
        logger.info(e.getMessage());
    }
    return error();
}
   
private boolean createMp4(String mp4SavePath, Map<Integer, URL> imgMap) throws FrameRecorder.Exception {
    //logger.info("[1]=======开始合成=======");
    long startTime=System.currentTimeMillis();
    //视频宽高最好是按照常见的视频的宽高  16:9  或者 9:16
    FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, VideoConstant.VIDEO_WIDTH,VideoConstant.VIDEO_HEIGHT);
    //设置视频编码层模式
    recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
    //设置视频为24帧每秒
    recorder.setFrameRate(VideoConstant.VIDEO_FRAME);
    //设置视频图像数据格式
    recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
    recorder.setAudioQuality(0);
    recorder.setFormat("mp4");
    try {
        recorder.start();
        Java2DFrameConverter converter = new Java2DFrameConverter();
        //int len=imgMap.size();
        int len=22
        for (int i = 0; i < len; i++) {
            BufferedImage read=ImageIO.read(imgMap.get(i));
            for (int j = 0; j < VideoConstant.VIDEO_FRAME; j++) {
                recorder.record(converter.getFrame(read));
            }
        }
        //logger.info("[2]生成视频结束==>"+(System.currentTimeMillis()-startTime));
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    } finally {
        //最后一定要结束并释放资源
        recorder.stop();
        recorder.release();
    }
    return true;
}

参考原文链接:JAVA 使用 JAVACV 实现图片合成短视频,并给视频添加音频!!! - 链滴 (ld246.com) 原文是以本地图片作为资源地址,该例子是以网络图片作为资源

2. java 使用cmd命令调用ffmpeg

maven依赖

<dependency>
   <groupId>ws.schild</groupId>
   <artifactId>jave-all-deps</artifactId>
   <version>3.3.1</version>
</dependency>

精简依赖

<dependency>
   <groupId>ws.schild</groupId>
   <artifactId>jave-core</artifactId>
   <version>3.3.1</version>
</dependency>

<!-- 在windows上开发 开发机可实现压缩效果 window64位 -->
<dependency>
   <groupId>ws.schild</groupId>
   <artifactId>jave-nativebin-win64</artifactId>
   <version>3.3.1</version>
</dependency>

<!-- 在linux上部署 linux服务器需要这个才能生效 linux64位 -->
<dependency>
   <groupId>ws.schild</groupId>
   <artifactId>jave-nativebin-linux64</artifactId>
   <version>3.3.1</version>
</dependency>
//cmd工具类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ws.schild.jave.process.ProcessKiller;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class FfmpegUtils {
    private static final Logger LOG = LoggerFactory.getLogger(ProcessWrapper.class);

    private  Process ffmpeg = null;

    private  ProcessKiller ffmpegKiller = null;

    private  InputStream inputStream = null;

    private  OutputStream outputStream = null;

    private  InputStream errorStream = null;

    public void execute(boolean destroyOnRuntimeShutdown, boolean openIOStreams, String ffmpegCmd) throws IOException {
        DefaultFFMPEGLocator defaultFFMPEGLocator = new DefaultFFMPEGLocator();
        StringBuilder cmd = new StringBuilder(defaultFFMPEGLocator.getExecutablePath());
        cmd.append(" ");
        cmd.append(ffmpegCmd);
        String cmdStr = String.format("ffmpegCmd final is :%s", cmd);
        LOG.info(cmdStr);

        Runtime runtime = Runtime.getRuntime();
        try {
            ffmpeg = runtime.exec(cmd.toString());
            if (destroyOnRuntimeShutdown) {
                ffmpegKiller = new ProcessKiller(ffmpeg);
                runtime.addShutdownHook(ffmpegKiller);
            }
            if (openIOStreams) {
                inputStream = ffmpeg.getInputStream();
                outputStream = ffmpeg.getOutputStream();
                errorStream = ffmpeg.getErrorStream();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public  InputStream getInputStream() {
        return inputStream;
    }

    public  OutputStream getOutputStream() {
        return outputStream;
    }

    public  InputStream getErrorStream() {
        return errorStream;
    }

    public  void destroy() {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (Throwable t) {
                LOG.warn("Error closing input stream", t);
            }
            inputStream = null;
        }

        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (Throwable t) {
                LOG.warn("Error closing output stream", t);
            }
            outputStream = null;
        }

        if (errorStream != null) {
            try {
                errorStream.close();
            } catch (Throwable t) {
                LOG.warn("Error closing error stream", t);
            }
            errorStream = null;
        }

        if (ffmpeg != null) {
            ffmpeg.destroy();
            ffmpeg = null;
        }

        if (ffmpegKiller != null) {
            Runtime runtime = Runtime.getRuntime();
            runtime.removeShutdownHook(ffmpegKiller);
            ffmpegKiller = null;
        }
    }

    public  int getProcessExitCode() {
        // Make sure it's terminated
        try {
            ffmpeg.waitFor();
        } catch (InterruptedException ex) {
            LOG.warn("Interrupted during waiting on process, forced shutdown?", ex);
        }
        return ffmpeg.exitValue();
    }

    public  void close() {
        destroy();
    }
}

原文链接:java - JAVA无需本地下载Ffmpeg,实现FfmpegCMD_个人文章 - SegmentFault 思否

import org.apache.poi.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
//文件处理工具类
public class ImageUtils
{
    private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);

    public static byte[] getImage(String imagePath)
    {
        InputStream is = getFile(imagePath);
        try
        {
            return IOUtils.toByteArray(is);
        }
        catch (Exception e)
        {
            log.error("图片加载异常 {}", e);
            return null;
        }
        finally
        {
            IOUtils.closeQuietly(is);
        }
    }

    public static InputStream getFile(String imagePath)
    {
        try
        {
            byte[] result = readFile(imagePath);
            result = Arrays.copyOf(result, result.length);
            return new ByteArrayInputStream(result);
        }
        catch (Exception e)
        {
            log.error("获取图片异常 {}", e);
        }
        return null;
    }

    /**
     * 读取文件为字节数据
     * 
     * @param url 地址
     * @return 字节数据
     */
    public static byte[] readFile(String url)
    {
        InputStream in = null;
        try
        {
            // 网络地址
            URL urlObj = new URL(url);
            URLConnection urlConnection = urlObj.openConnection();
            urlConnection.setConnectTimeout(30 * 1000);
            urlConnection.setReadTimeout(60 * 1000);
            urlConnection.setDoInput(true);
            in = urlConnection.getInputStream();
            return IOUtils.toByteArray(in);
        }
        catch (Exception e)
        {
            log.error("访问文件异常 {}", e);
            return null;
        }
        finally
        {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * @description:
     * @param url: 网络地址
     * @param url: 保存地址
     * @return: void
     */
    public static void nioDownloadDoc(String url,String path) throws IOException {
        URL urlObj = new URL(url);
        URLConnection urlConnection = urlObj.openConnection();
        urlConnection.setConnectTimeout(30 * 1000);
        urlConnection.setReadTimeout(60 * 1000);
        urlConnection.setDoInput(true);
        FileOutputStream fileOutputStream=new FileOutputStream(path);
        //获取输出流通道
        WritableByteChannel writableByteChannel = Channels.newChannel(fileOutputStream);
        ReadableByteChannel readableByteChannel = Channels.newChannel(new BufferedInputStream(urlConnection.getInputStream()));
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (-1 != readableByteChannel.read(buf)) {
            buf.flip();
            writableByteChannel.write(buf);
            buf.clear();
        }
        fileOutputStream.flush();
        writableByteChannel.close();
        readableByteChannel.close();
    }
}
public AjaxResult composeTemp() {
    //资源存放目录
    String path="D:\\image"+File.separator+ IdUtil.fastSimpleUUID();
    try {
        //实际存放地址
        String mp4SavePath = "D:\\temp"+File.separator+ IdUtil.fastSimpleUUID() +".mp4";
        //logger.info("[1]=======定制模板中=======");
        //查询结果集
        List<String> urls=new ArrayList<>();
        if (urls.isEmpty()) {
            return AjaxResult.error("该模板维护中,无法生成视频");
        }
        File file=new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        //资源文件命名,方便ffmpeg获取,由于ffmpeg获取本地资源,网络图片需下载,方便后续使用多线程下载扩展
        /*for (int i = 0; i < urls.size(); i++) {
            urlMap.put(i,urls.get(i));
        }
        for (Map.Entry<Integer, String> entry : urlMap.entrySet()) {
            byte[] data = ImageUtils.getImage(entry.getValue());
            FileUtil.writeAndClose(data, path + File.separator + entry.getKey() + ".jpg");
        }*/
        int index=1;
        //long startTime=System.currentTimeMillis();
        //logger.info("[2]=======下载资源图片中=======");
        for (int i = 0; i < urls.size(); i++) {
            byte[] data= ImageUtils.getImage(urls.get(i));
            FileOutputStream fileOutputStream=new FileOutputStream(path+ File.separator+index+".jpg");
            IOUtils.write(data,fileOutputStream);
            fileOutputStream.close();
            index++;
        }
        //logger.info("下载结束==>"+(System.currentTimeMillis()-startTime));
        //logger.info("[3]=======合成视频中=======");
        //分辨率1600x900(推荐使用固定分辨率,这里犯过错了,错误截图未保存)
        Integer code=executeFfmpeg(path,mp4SavePath,1600,900);
        //logger.info("ffmpeg退出返回:{}",code);
        if (code == 0) {
            //逻辑处理

            return AjaxResult.success();
        }
    } catch (IOException e) {
        //logger.info(e.getMessage());
    }finally {
        //logger.info("[6].删除临时文件:{},结果:{}",path,delFlag);
    }
    return AjaxResult.error();
}

private Integer executeFfmpeg(String path,String mp4SavePath, int videoWidth, int videoHeight) throws IOException {
    long startTime=System.currentTimeMillis();
    FfmpegUtils ffmpegUtils= new FfmpegUtils();
    StringBuilder cmdBuilder=new StringBuilder()
            .append("-y").append(" ").append("-r").append(" ").append(1).append(" ")
            .append("-f").append(" ").append("image2").append(" ").append("-i").append(" ")
            .append(path).append("/%d.jpg").append(" ")
            .append("-s").append(" ")
            .append(videoWidth).append("x").append(videoHeight).append(" ")
            .append("-c:a").append(" ")
            .append("copy").append(" ")
            .append(mp4SavePath);
    ffmpegUtils.execute(false,true,cmdBuilder.toString());
    InputStream errorStream = ffmpegUtils.getErrorStream();
    //打印过程
    int len = 0;
    while ((len=errorStream.read())!=-1){
        System.out.print((char) len);
    }
    System.out.print("合成结束==>"+(System.currentTimeMillis()-startTime));
    //code=0表示正常
    Integer code=ffmpegUtils.getProcessExitCode();
    ffmpegUtils.close();
    return code;
}

打印命令 ffmpegCmd final is :C:\Users\***\AppData\Local\Temp\jave\ffmpeg-amd64-3.3.1.exe -threads 2 -y -r 1 -f image2 -i D:\image\503981913856444f82434b2d6cd8c2cf/%d.jpg -s 1600x900 -c:a copy D:\temp\4733f69b7d664a7f9770f635b282b2bf.mp4

-y 表示覆盖 -r 表示帧率(在-i前后有区别) image2 表示照片合成视频 %d表示文件名是数字 -s表示分辨率 -c:a copy 按原格式输出

网上还有获取网络URL的方式,后续看看如何写个demo,继续优化这个帖子,本人不善输出,慢慢改正