添加jar包依赖
<!-- 视频截图 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.4.2</version>
</dependency>
<!-- gif -->
<dependency>
<groupId>com.madgag</groupId>
<artifactId>animated-gif-lib</artifactId>
<version>1.4</version>
</dependency>
Gif生成工具类
package org.pet.king.util;
importjava.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import com.madgag.gif.fmsware.AnimatedGifEncoder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GifUtil {
/**
* 默认每截取一次跳过多少帧(默认:2)
*/
private static final Integer DEFAULT_MARGIN = 2;
/**
* 默认帧频率(默认:10)
*/
private static final Integer DEFAULT_FRAME_RATE = 10;
/**
* 截取视频指定帧生成gif,存储路径同级下
*
* @param videofile
* 视频文件
* @param startFrame
* 开始帧
* @param frameCount
* 截取帧数
* @param frameRate
* 帧频率(默认:2)
* @param margin
* 每截取一次跳过多少帧(默认:10)
* @throws IOException
*
*/
public static String buildGif(String filePath, int startFrame, int frameCount, Integer frameRate, Integer margin)
throws IOException {
if (margin == null) {
margin = DEFAULT_MARGIN;
}
if (frameRate == null) {
frameRate = DEFAULT_FRAME_RATE;
}
// gif存储路径
String gifPath = filePath.substring(0, filePath.lastIndexOf(".")) + ".gif";
// 输出文件流
FileOutputStream targetFile = new FileOutputStream(gifPath);
// 读取文件
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(filePath);
Java2DFrameConverter converter = new Java2DFrameConverter();
// 无限期的循环下去、注意,此参数设置必须在下面for循环之前,即在添加第一帧数据之前
en.setRepeat(0);
ff.start();
try {
Integer videoLength = ff.getLengthInFrames();
// 如果用户上传视频极短,不符合自己定义的帧数取值区间,设置合法区间
if (startFrame > videoLength || (startFrame + frameCount * margin) > videoLength) {
startFrame = videoLength / 5;
frameCount = (videoLength - startFrame - 5) / margin;
}
ff.setFrameNumber(startFrame);
AnimatedGifEncoder en = new AnimatedGifEncoder();
en.setFrameRate(frameRate);
en.start(targetFile);
for (int i = 0; i < frameCount; i++) {
en.addFrame(converter.convert(ff.grabFrame()));
ff.setFrameNumber(ff.getFrameNumber() + margin);
}
en.finish();
} finally {
ff.stop();
ff.close();
}
log.info("返回文件路径");
return gifPath;
}
public static void main(String[] args) {
try {
System.out.println(buildGif("E:/file/20190722085411_IMG_8131.MP4", 5, 50, 10, 2));
} catch (Exception e) {
e.printStackTrace();
// TODO: handle exception
}
}
}
理论上而言执行main方法就可以生成gif文件,但是有的时候你会发现虽然gif生成了,但是文件错误,无法打开(例如mov文件),这就是我遇到的关键问题所在,以下为解决办法:
我对工具类加了大量的日志打印以及断点,如下:
通过两台电脑同时跑(一个mp4文件,一个mov文件,发现问题出现在ff.setFrameNumber(startFrame);
这里)
现在进入这个方法看一下流程是什么:
@Override public void setFrameNumber(int frameNumber) throws Exception {
if (hasVideo()) setTimestamp(Math.round(1000000L * frameNumber / getFrameRate()));
else super.frameNumber = frameNumber;
}
上述为目标方法的代码,如果你使用mov文件生成gif执行到这里你会发现hasVideo()返回值是false,而hasVideo()方法判断的是video_st参数值,下一个任务就是查看video_st参数在什么时候设置的值了,目标代码是ff.start();
,参数设置的位置在FFmpegFrameGrabber
类的772行左右
会发现mov执行else if条件语句,mp4执行if条件语句,导致mov文件被认为是音频文件,但这里还不是重点,就算你设置了startFrame为0又怎样,虽然第一帧是黑的,但是我截取一部分总会有不黑的部分吧,继续向下调试,发现converter.convert(ff.grab())
打印的数据是null,而mp4时则不是null,问题应该就出现在这里,执行到这里的时候我们查看ff参数的各属性,你会发现ff的frame属性存在不同,mov格式时代码执行到ff.setFrameNumber(startFrame);
时frameNumber变为0,但是frame里面无变化,而mp4格式时frameNumber为初始化的5,同时你会发现frame属性里面image值变化了!!!!!
重点就在这里,mov时被当作音频处理,image属性始终为null,现在进入到converter.convert(ff.grabFrame())
方法,
第一个条件判断直接返回null了,所以gif无论如何肯定无法生成了,向回回溯,问题点在于mov文件被当成音频处理了,我的解决办法是在文件上传时对文件进行转码,只要是mov后缀名的文件统一转一份mp4格式的视频文件,这样生成的时候使用mp4的视频文件生成即可。
文件转码
添加依赖
<dependency>
<groupId>com.github.dadiyang</groupId>
<artifactId>jave</artifactId>
<version>1.0.5</version>
</dependency>
maven仓库中没有jave-1.0.2的jar包,此处暂用上述依赖,当然你也可以下载jar包然后mvn install
文件转码工具类
package org.pet.king.util;
import java.io.File;
import it.sauronsoftware.jave.AudioAttributes;
import it.sauronsoftware.jave.Encoder;
import it.sauronsoftware.jave.EncodingAttributes;
import it.sauronsoftware.jave.VideoAttributes;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FileEncodeUtil {
/**
* 文件转码
*
* @param source
* 源文件路径
* @param target
* 目标文件路径
* @return 转换是否成功
* @author single-聪
* @date 2019年7月22日
* @version 1.0.1
*/
public static boolean encoder(String sourcePath, String targetPath) {
File source = new File(sourcePath);
File target = new File(targetPath);
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
audio.setBitRate(new Integer(56000));
audio.setChannels(new Integer(1));
audio.setSamplingRate(new Integer(22050));
VideoAttributes video = new VideoAttributes();
video.setCodec("mpeg4");
// video.setSize(new VideoSize(400, 300));
video.setBitRate(new Integer(800000));
video.setFrameRate(new Integer(15));
EncodingAttributes attrs = new EncodingAttributes();
attrs.setFormat("mp4");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
Encoder encoder = new Encoder();
try {
encoder.encode(source, target, attrs);
return true;
} catch (Exception e) {
log.info("视频转码失败");
e.printStackTrace();
return false;
}
}
}
如果是mov格式文件调用上述方法即可生成一个mp4格式视频,然后根据mp4格式视频生成gif动图即可,建议视频转码完成之后删除上一文件。
下面是生成视频预览图的方法,多数前端框架使用poster标签定义,在这里说一下,阿里的OSS文件存储提供这个服务,所以一般情况下可以直接使用阿里提供的服务,服务费可以忽略不计,但是使用这个貌似无法读取到视频的宽高(有些前端框架播放视频的时候需要用到)所以在这里也提供一下这个方法
生成预览图的原理和制作GIF一样,因为GIF我们是使用多个Image拼接到一起的,所以预览图我们只需要截取到第一帧有画面的即可(一般情况下是第二帧)现在重写buildGif方法如下:
/**
* 截取视频指定帧生成gif,存储路径同级下
*
* @param filePath
* 视频文件路径
* @param startFrame
* 开始帧
* @param frameCount
* 截取帧数
* @param frameRate
* 帧频率(默认:3)
* @param margin
* 每截取一次跳过多少帧(默认:3)
* @throws IOException
*
*/
public static FileResponse buildGif(String filePath, int startFrame, int frameCount, Integer frameRate,
Integer margin) throws IOException {
FileResponse file = new FileResponse();
if (margin == null) {
margin = DEFAULT_MARGIN;
}
if (frameRate == null) {
frameRate = DEFAULT_FRAME_RATE;
}
// gif存储路径
String gifPath = filePath.substring(0, filePath.lastIndexOf(".")) + ".gif";
// 输出文件流
FileOutputStream targetFile = new FileOutputStream(gifPath);
// 读取文件
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(filePath);
Java2DFrameConverter converter = new Java2DFrameConverter();
ff.start();
try {
Integer videoLength = ff.getLengthInFrames();
log.info("视频帧长度:[{}]", videoLength);
// 如果用户上传视频极短,不符合自己定义的帧数取值区间,那么获取从1/5处开始至1/2处结束生成gif
if (startFrame > videoLength || (startFrame + frameCount * margin) > videoLength) {
startFrame = videoLength / 5;
frameCount = (videoLength - startFrame - 5) / margin;
}
ff.setFrameNumber(startFrame);
log.info("起始位置[{}]帧数:[{}]跳步:[{}]", startFrame, frameCount, margin);
AnimatedGifEncoder en = new AnimatedGifEncoder();
en.setFrameRate(frameRate);
log.info("帧频率设置为[{}]", frameRate);
// 无限期的循环下去、注意,此参数设置必须在下面for循环之前,即在添加第一帧数据之前
en.setRepeat(0);
en.start(targetFile);
// 预览图、当前未生成
boolean poster = false;
for (int i = 0; i < frameCount; i++) {
// BufferedImage image = (BufferedImage)
// converter.convert(ff.grabFrame()).getScaledInstance(300, 400,
// Image.SCALE_DEFAULT);
// log.info("图片质量压缩");
// 截取一帧,确保截取的当前帧存在图片!
if (!poster) {
Frame f = ff.grabFrame();
if (f != null) {
// 图片宽高即为视频宽高
file.setHeight(f.imageHeight);
file.setWidth(f.imageWidth);
File filePicture = new File(
filePath.substring(0, filePath.lastIndexOf(".")) + ",jpg");
log.info("vedio参数为[{}]", file);
log.info("文件参数:[{}]", filePicture);
// 获取图片信息
BufferedImage image = (BufferedImage) converter.getBufferedImage(f);
BufferedImage bi = new BufferedImage(file.getWidth(), file.getHeight(),
BufferedImage.TYPE_3BYTE_BGR);
bi.getGraphics().drawImage(
image.getScaledInstance(file.getWidth(), file.getHeight(), Image.SCALE_DEFAULT), 0, 0,
null);
// 生成视频预览图
ImageIO.write(image, "jpg", filePicture);
poster = true;
file.setPosterUrl(filePicture.getPath());
}
}
en.addFrame(converter.convert(ff.grabFrame()));
// log.info("取帧位置[{}],参数[{}]", frameCount, ff.grabFrame());
ff.setFrameNumber(ff.getFrameNumber() + margin);
// log.info("设置下一帧位置:[{}]", ff.getFrameNumber());
}
en.finish();
} finally {
ff.stop();
ff.close();
}
log.info("上传gif图片到oss文件存储,返回gif文件存储路径");
file.setGifUrl(gifPath);
return file;
}
上述方法实现截取第一个有画面的图片作为预览图,同时获取到图片宽高,保存路径自己根据情况设置即可,上述方法的GIF和图片和视频位于相同目录下,文件名相同后缀不同。
FileResponse是我自定义的一个类,用以存放根据这个视频生成的相关文件的信息,如下(根据个人需求更改):
package org.pet.king.response;
import lombok.Data;
@Data
public class FileResponse {
/**
* 是否转码成功,默认成功
*/
private boolean encode = true;
/**
* gif创建是否成功
*/
private boolean gif = true;
/**
* 本地gif文件路径
*/
private String gifUrl;
/**
* 本地视频路径
*/
private String url;
/**
* 预览图本地存储路径
*/
private String posterUrl;
/**
* 视频高
*/
private Integer height = 400;
/**
* 视频宽
*/
private Integer width = 300;
public FileResponse() {
super();
}
}