文章目录
- 前言
- 一、JavaCv是什么?
- 二、使用步骤
- 1.引入库
- 2.获取本地视频的截图并保存
- 3.获取网络视频流的截图并保存
- 总结
前言
这两天项目中有个需求是从视频流中定时截图保存至服务器,之前没操作过视频,大致花了半天查网上的资料,做了个可以从视频中截图的demo,仅供参考
一、JavaCv是什么?
JavaCv:是一个工具类,里面封装了对视频进行操作的方法。说JavaCv,就不得不说一下FFmpeg、OpenCV、JavaCPP,我们使用的JavaCv都是对这三个的封装
工具 | 特点 | java’是否可以直接使用 |
---|---|---|
FFmpeg | C语言版的本地库 | 否 |
OpenCV | C语言版的本地库 | 否 |
JavaCPP | 对FFmpeg、OpenCV这些常用库进行包装,成为API | 是 |
JavaCv | 对JavaCPP进行封装,成为工具类,比JavaCpp更简单易用 | 是 |
我们本次就使用JavaCv来操作视频流,因为JavaCv底层也是使用FFmpeg,内部原理都是一致的,所以可以先学习用JavaCPP操作FFmpeg获取视频流的方式,从中学习原理,便于更好的理解FFmpeg。大佬已经做了很完整的demo攻略,大家可以参考学习:Java版流媒体编解码和图像处理(JavaCPP+FFmpeg)
二、使用步骤
1.引入库
在pom文件中引入包ffmpeg、javacpp、javacv:
<!-- 视频截图 ffmpeg javacpp javacv -->
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>4.0.2-1.4.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>1.4.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.4.3</version>
</dependency>
2.获取本地视频的截图并保存
- 在D盘创建一个两个文件夹D:\files\video、D:\files\img,在video下放一个MP4格式的视频文件,供截图使用;img下存储的为截图文件,文件名为当前截图时间;获取本地视频文件的第六桢截图保存至本地文件中
代码如下(示例):
package com.example.common.util.video;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacpp.opencv_core;
import org.bytedeco.javacv.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author sliu
* @date 2023/6/13 10:26
**/
@Slf4j
public class VideoUtil {
public static void main(String[] args) throws IOException {
getScreenshotByFile(
"D:\\files\\video\\blue.mp4",
"D:\\files\\img\\" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg");
}
/**
* 获取指定视频文件的帧(过滤前5帧)并保存为图片至指定目录
* @param videoFile 源视频文件路径
* @param imgFile 截取帧的图片存放路径
* @throws FrameGrabber.Exception 异常
* @throws IOException io异常
*/
public static void getScreenshotByFile(String videoFile, String imgFile)
throws FrameGrabber.Exception, IOException {
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(videoFile);
ff.start();
//视频的旋转角度
String rotate =ff.getVideoMetadata("rotate");
int lenght = ff.getLengthInFrames();
int i = 0;
Frame f = null;
while (i < lenght) {
//过滤前5帧,避免出现全黑的图片,使用grabImage而不是grabFrame,可以避免mov格式的视频截图失败
f = ff.grabImage();
if ((i > 5) && (f.image != null)) {
break;
}
opencv_core.IplImage src = null;
if(null !=rotate &&rotate.length() > 1) {
OpenCVFrameConverter.ToIplImage converter =new OpenCVFrameConverter.ToIplImage();
src =converter.convert(f);
f =converter.convert(rotate(src, Integer.parseInt(rotate)));
}
doExecuteFrame(f, imgFile);
i++;
}
}
/**
* 根据视频旋转度来调整图片
* @param src 图片
* @param angle 旋转角度
* @return IplImage 旋转后的图片
*/
public static opencv_core.IplImage rotate(opencv_core.IplImage src, int angle) {
opencv_core.IplImage img = opencv_core.IplImage.create(src.height(),src.width(),src.depth(),src.nChannels());
opencv_core.cvTranspose(src,img);
opencv_core.cvFlip(img,img,angle);
return img;
}
/**
* 把截取的视频流写入图片中
* @param f 截取的视频流
* @param targetFileName 目标文件名称
*/
public static void doExecuteFrame(Frame f,String targetFileName) {
if (null ==f ||null ==f.image) {
return;
}
Java2DFrameConverter converter =new Java2DFrameConverter();
BufferedImage bi =converter.getBufferedImage(f);
File output =new File(targetFileName);
try {
ImageIO.write(bi,"jpg",output);
}catch (IOException e) {
e.printStackTrace();
}
}
}
- 运行main方法后,控制台打印如下,代表截图成功
- 查看D:\files\img下文件是否存在截图图片
3.获取网络视频流的截图并保存
- 找一个网络视频流地址(我这属于私人视频流地址,就不放出来了);在D盘创建一个文件夹D:\files\img,存储的为截图文件,文件名为当前截图时间;从该视频流中默认获取第一帧,截图保存到文件中
代码如下(示例):
package com.example.common.util.video;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author sliu
* @date 2023/6/13 10:26
**/
@Slf4j
public class VideoUtil {
public static void main(String[] args) throws IOException {
getScreenshotByUrl(
"此处为可播放的流媒体地址",
"D:\\files\\img\\" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg");
}
/**
* 获取指定视频地址的第一帧并保存为图片至指定目录
*
* @param url 视频文件路径
* @param imgFile 图片保存地址(含文件名)
*/
public static void getScreenshotByUrl(String url,String imgFile) throws FrameGrabber.Exception {
FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(url);
try {
log.debug("截取视频截图开始:,视频地址为:{}" + System.currentTimeMillis(),url);
grabber.start();
//设置视频截取帧(默认取第一帧)
Frame frame = grabber.grabImage();
Java2DFrameConverter converter = new Java2DFrameConverter();
//绘制图片
BufferedImage image = converter.getBufferedImage(frame);
if(StrUtil.isNotBlank(grabber.getVideoMetadata("rotate"))){
image = rotate(image, Integer.parseInt(grabber.getVideoMetadata("rotate")));
}
try {
File output =new File(imgFile);
ImageIO.write(image,"jpg",output);
}catch (IOException e) {
e.printStackTrace();
}
log.error("视频截图成功");
} catch (FrameGrabber.Exception e) {
log.error("视频截图失败{}", e.getMessage());
} catch (IOException e) {
e.printStackTrace();
} finally {
grabber.stop();
grabber.close();
}
}
/**
* 根据视频旋转度来调整图片
*
* @param src BufferedImage
* @param angel angel 视频旋转度
* @return BufferedImage
*/
public static BufferedImage rotate(BufferedImage src, int angel) {
int srcWidth = src.getWidth(null);
int srcHeight = src.getHeight(null);
int type = src.getColorModel().getTransparency();
Rectangle rectDes = calcRotatedSize(new Rectangle(new Dimension(srcWidth, srcHeight)), angel);
BufferedImage bi = new BufferedImage(rectDes.width, rectDes.height, type);
Graphics2D g2 = bi.createGraphics();
g2.translate((rectDes.width - srcWidth) / 2, (rectDes.height - srcHeight) / 2);
g2.rotate(Math.toRadians(angel), srcWidth / 2, srcHeight / 2);
g2.drawImage(src, 0, 0, null);
g2.dispose();
return bi;
}
/**
* 计算图片旋转大小
*
* @param src Rectangle
* @param angel int
* @return Rectangle
*/
public 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 angelAlpha = (Math.PI - Math.toRadians(angel)) / 2;
double angelDeltaWidth = Math.atan((double) src.height / src.width);
double angelDeltaHeight = Math.atan((double) src.width / src.height);
int lenDeltaWidth = (int) (len * Math.cos(Math.PI - angelAlpha - angelDeltaWidth));
int lenDeltaHeight = (int) (len * Math.cos(Math.PI - angelAlpha - angelDeltaHeight));
int desWidth = src.width + lenDeltaWidth * 2;
int desHeight = src.height + lenDeltaHeight * 2;
return new java.awt.Rectangle(new Dimension(desWidth, desHeight));
}
}
- 运行main方法后,控制台打印如下,代表截图成功
- 查看D:\files\img下文件是否存在截图图片(红框为网络视频流截图图片)
总结
以上就是使用JavaCv工具类对流媒体解码存图的demo,大家可以尝试用JavaCpp的方式也做一个demo,可以更好的理解内部原理。当然,真正项目开发上,肯定会偏向使用更简洁代码量更小更通俗易懂的工具类,比如JavaCv。或者大家有什么更好的方式,代码更简洁的方法,也可以分享出来大家一起学习,一起进步!
文章出处登录后可见!
已经登录?立即刷新