最近做的一个照片合成视频的需求,记下笔记。
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,继续优化这个帖子,本人不善输出,慢慢改正