1.需求背景:
相机上传的视频,没有上传封面,需要后台对云存储上的视频,添加封面后,保存封面和视频地址在数据库中。
2. JavaCV 简介
JavaCV是对各种常用计算机视觉库的封装后的一组jar包,其中封装了FFmpeg、OpenCV等计算机视觉编程人员常用库的接口,可以通过其中的Utility类方便的在包括Android在内的Java平台上调用这些接口。其中使用最多的应该就是FFmpeg了。
最开始Javacv是googlecode下面的一个项目,后来迁移到了github,因此JavaCV相关的包名也由com.googlecode.javacv
改为org.bytedeco.javacv。
项目地址:https://github.com/bytedeco/javacv
3. JavaCV 基本使用指南
从github项目中下载打包好的jar包,其中javacpp.jar 和 javacv.jar是必须的两个包,然后需要什么功能就添加额外的包。例如我需要用到ffmpeg进行视频录制,则需要添加ffmpeg.jar和ffmpeg-android-arm.jar(打包的arm平台so库)。这里很方便的地方是,so库文件都放在了jar文件里面,在集成的时候很简洁明了。
4.javacv的使用
1.4.x、1.3.x版本的精简jar:
<!--javacv 截取视频封面包-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.4.3</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-1.4.2</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-1.4.2</version>
<classifier>windows-x86_64</classifier>
<!-- <classifier>linux-x86_64</classifier>-->
</dependency>
<!-- https://mvnrepository.com/artifact/org.bytedeco.javacpp-presets/ffmpeg -->
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-1.4.2</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-1.4.2</version>
<classifier>windows-x86_64</classifier>
<!-- <classifier>linux-x86_64</classifier>-->
</dependency>
<!---->
1.5.x版本后的精简包:
<!--javacv 截取视频封面包-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.7</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>5.0-1.5.7</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>5.0-1.5.7</version>
<!-- <classifier>windows-x86_64</classifier>-->
<classifier>linux-x86_64</classifier>
</dependency>
<!---->
2.实现:
package com.camojojo.device.common.utils;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import com.camojojo.common.core.utils.AwsFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
/**
* @author:yuchen
* @createTime:2022/7/11 16:07
*/
@Slf4j
public class VideoUtil {
private static final String PATH="/ossRoot/";
public static void main(String[] args) {
getVideoPic();
}
public static String getVideoPic() {
String url = "https://xxx/xxxx.mp4";
String value = "1111123";
String dir = value +"/";
String fileName = FileUtil.getName(url);// 100.mp4
String targetFileName = "thumb_"+fileName.substring(0, fileName.lastIndexOf("."))+".jpg";
File filePath = new File(PATH+dir);
if (!filePath.exists()) {
boolean mkdirs = filePath.mkdirs();
if(mkdirs){
log.info("文件夹创建成功:{}",filePath.getAbsolutePath());
}
}
String outPath = PATH+dir+targetFileName;
File outFile = getScreenshot(url, outPath);
if(outFile != null){
String fileName1 = AwsFileUtil.uploadS3Us(outFile, imei, targetFileName);
System.out.println(fileName1);
boolean delete = outFile.delete();
if(delete){
log.info("删除本地临时文件成功");
}
return fileName1;
}
return "";
}
/**
* 获取视频封面图
* @param filePath 视频地址
* @return map
*/
private static File getScreenshot(String filePath, String imagePath) {
log.info("截取视频截图开始:Video={}",filePath);
filePath = filePath.replaceAll("\\\\","/");
File output = null;
FFmpegFrameGrabber grabber = null;
try{
grabber = FFmpegFrameGrabber.createDefault(filePath);
grabber.start();
//设置视频截取帧(建议从5帧开始,防止全是黑屏)
Frame frame = null;
for (int j = 0; j < 5; j++) {
frame = grabber.grabImage();
}
//视频旋转度
String rotate = grabber.getVideoMetadata("rotate");
Java2DFrameConverter converter = new Java2DFrameConverter();
//绘制图片
BufferedImage bi = converter.getBufferedImage(frame);
if (rotate != null) {
// 旋转图片
bi = rotate(bi, Integer.parseInt(rotate));
}
//创建文件
output = new File(imagePath);
ImgUtil.write(bi, output);
log.info("截取视频截图成功");
}catch (Exception e){
log.error("获取视频封面图,Error:",e);
}finally {
if(grabber!=null){
try { grabber.stop(); } catch (Exception ignored) { }
}
}
return output;
}
/**
* @Description 根据视频旋转度来调整图片
* @param src ""
* @param angel 视频旋转度
* @return BufferedImage
*/
private static BufferedImage rotate(BufferedImage src, int angel) {
int src_width = src.getWidth(null);
int src_height = src.getHeight(null);
int type = src.getColorModel().getTransparency();
Rectangle rect_des = calcRotatedSize(new Rectangle(new Dimension(src_width, src_height)), angel);
BufferedImage bi = new BufferedImage(rect_des.width, rect_des.height, type);
Graphics2D g2 = bi.createGraphics();
g2.translate((rect_des.width - src_width) / 2, (rect_des.height - src_height) / 2);
g2.rotate(Math.toRadians(angel), src_width / 2, src_height / 2);
g2.drawImage(src, 0, 0, null);
g2.dispose();
return bi;
}
/**
* @Description: 计算图片旋转大小
* @param src ""
* @param angel ""
* @return Rectangle
*/
private static Rectangle calcRotatedSize(Rectangle src, int angel) {
if (angel >= 90) {
if (angel / 90 % 2 == 1) {
int temp = src.height;
src.height = src.width;
src.width = temp;
}
angel = angel % 90;
}
double r = Math.sqrt(src.height * src.height + src.width * src.width) / 2;
double len = 2 * Math.sin(Math.toRadians(angel) / 2) * r;
double angel_alpha = (Math.PI - Math.toRadians(angel)) / 2;
double angel_dalta_width = Math.atan((double) src.height / src.width);
double angel_dalta_height = Math.atan((double) src.width / src.height);
int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
int des_width = src.width + len_dalta_width * 2;
int des_height = src.height + len_dalta_height * 2;
return new java.awt.Rectangle(new Dimension(des_width, des_height));
}
}
5.使用Docker部署后,截屏异常
在另一个Docker部署的服务中使用该方法,发现一调用,服务就挂。
最后发现原因是由于那个Docker容器使用的是阉割版的JDK镜像:openjdk:8-jre-alpine ,虽然该镜像能使打包的体积更小,但是容易出现此类的问题,需要换成更完整的JDK镜像。
后面换成: openjdk:8-jre ,就没问题了。
javacv版本1.4.x ,会出现物理内存溢出溢出,网上说是旧版本源码bug,升级到1.5.x版本就修复了,今天升级了,运行一段时间再看。
6.1.5.7获取不到视频旋转角度
改成1.5.6版本正常获取
特此记录。