Java截取视频生成Gif动图

添加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();
	}
}
发布了43 篇原创文章 · 获赞 25 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/single_cong/article/details/96863300