前言:
我们在很多时候需要对视频文件进行分析,或者对视频产生缩略图。因此视频截取技术必不可少。
从本地文件中读取视频帧
导包
xml
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec</artifactId>
<version>0.2.5</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
</dependency>
<!-- http://repo1.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
读取视频帧工具类
java
package com.wkl.testdemo.vedio;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.jcodec.api.FrameGrab;
import org.jcodec.api.JCodecException;
import org.jcodec.common.model.Picture;
import org.jcodec.scale.AWTUtil;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.UUID;
/**
* 视频操作工具类
*/
@Slf4j
public class VideoUtils {
/*** 图片格式*/
private static final String FILE_EXT = "jpg";
/*** 帧数-第几帧*/
private static final int THUMB_FRAME = 500;
/**
* 获取指定视频的帧并保存为图片至指定目录
*
* @param videoFilePath 源视频文件路径
* @param frameFilePath 截取帧的图片存放路径
*/
public static void fetchFrame(String videoFilePath, String frameFilePath) throws Exception {
File videoFile = new File(videoFilePath);
File frameFile = new File(frameFilePath);
getThumbnail(videoFile, frameFile);
}
/**
* 获取指定视频的帧并保存为图片至指定目录
*
* @param videoFile 源视频文件
* @param targetFile 截取帧的图片
*/
public static void fetchFrame(MultipartFile videoFile, File targetFile) throws Exception {
File file = new File(videoFile.getName());
FileUtils.copyInputStreamToFile(videoFile.getInputStream(), file);
getThumbnail(file, targetFile);
}
/**
* 获取指定视频的帧并保存为图片至指定目录
*
* @param videoFile 源视频文件
*/
public static File fetchFrame(MultipartFile videoFile) {
String originalFilename = videoFile.getOriginalFilename();
File file = new File(originalFilename);
File targetFile = null;
try {
FileUtils.copyInputStreamToFile(videoFile.getInputStream(), file);
int i = originalFilename.lastIndexOf(".");
String imageName;
if (i > 0) {
imageName = originalFilename.substring(0, i);
} else {
imageName = UUID.randomUUID().toString().replace("-", "");
}
imageName = imageName + ".jpg";
targetFile = new File(imageName);
getThumbnail(file, targetFile);
} catch (Exception e) {
log.error("获取视频指定帧异常:", e);
} finally {
if (file.exists()) {
file.delete();
}
}
log.debug("视频文件 - 帧截取 - 处理结束");
return targetFile;
}
/**
* 获取第一帧缩略图
*
* @param videoFile 视频路径
* @param targetFile 缩略图目标路径
*/
public static void getThumbnail(File videoFile, File targetFile) {
try {
// 根据扩展名创建一个新文件路径
Picture picture = FrameGrab.getFrameFromFile(videoFile, THUMB_FRAME);
BufferedImage bufferedImage = AWTUtil.toBufferedImage(picture);
ImageIO.write(bufferedImage, FILE_EXT, targetFile);
} catch (IOException | JCodecException e) {
e.printStackTrace();
log.error("获取第一帧缩略图异常:", e);
}
}
public static void main(String[] args) {
try {
long startTime = System.currentTimeMillis();
getThumbnail(new File("D:\\Videos\\2023112911533869304715.mp4"), new File("D:\\Videos\\test1.jpg"));
System.out.println("截取图片耗时:" + (System.currentTimeMillis() - startTime));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void saveImage(String imageUrl, String destinationFile) throws IOException {
URL url = new URL(imageUrl);
InputStream is = url.openStream();
OutputStream os = new FileOutputStream(destinationFile);
byte[] b = new byte[2048];
int length;
while ((length = is.read(b)) != -1) {
os.write(b, 0, length);
}
is.close();
os.close();
}
}
从网络url 读取视频帧-ffmpeg
导包
xml
<!-- 获取视频第一帧依赖 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>3.4.2-1.4.1</version>
</dependency>
工具类
java
package com.wkl.testdemo.vedio;
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.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
* 视频抽帧工具
*/
@Slf4j
public class VideoFrame {
//传入包含特定时间的Frame ,读取图片
public static BufferedImage doExecuteFrame(Frame frame, int index) {
if (frame == null || frame.image == null) {
return null;
}
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage bi = converter.getBufferedImage(frame);
return bi;
}
/*
* @description: 读取视频第n秒的图片帧
* @author: wangkanglu
* @date: 2023/12/8 15:05
* @param: [videoUrl, stepSecond]
* @return: java.awt.image.BufferedImage
**/
public static BufferedImage doExecuteFrameByTime(String videoUrl, Integer stepSecond) {
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(videoUrl);
ff.setOption("timeout", "40000000");
long timestamp = stepSecond * 1000000L; //视频是按照微秒计算的,10的6次方分之一秒
try {
ff.start();
ff.setTimestamp(timestamp);
Frame frame = ff.grabImage();
if (frame == null || frame.image == null) {
return null;
}
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage bi = converter.getBufferedImage(frame);
ff.stop();
return bi;
} catch (FrameGrabber.Exception e) {
throw new RuntimeException(e);
}
}
/**
* 视频文件边下载边抽帧1秒1帧
*
* @param videoUrl 网络视频文件URL
* @param stepSecond 每隔几秒取一帧,默认1s
* @param count 需要截取的帧个数
* @return
*/
public static Map<Integer, BufferedImage> videoUrlIntercept(String videoUrl, Integer stepSecond, Integer count) {
Map<Integer, BufferedImage> files = new HashMap<>();
stepSecond = stepSecond == null ? 1 : stepSecond;
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(videoUrl);
// 设置超时时间为40秒
ff.setOption("timeout", "40000000");
// ff.setOption("user_agent", UserAgent.getUserAgent());
try {
ff.start();
long timeLength = ff.getLengthInTime();
Frame frame = ff.grabImage();
long startTime = frame.timestamp;
long timestamp = 0; //视频的当前时长
int second = 0; //过了几个时间间隔
int picNum = 0;//第n张帧
while (timestamp <= timeLength) {
log.info("抽取第{}帧,video_url:{}",picNum,videoUrl);
timestamp = startTime + second * 1000000L; //视频是按照微秒计算的,10的6次方分之一秒
ff.setTimestamp(timestamp);
frame = ff.grabImage();
if (frame != null) {
if (frame.image != null) {
BufferedImage bufferedImage = doExecuteFrame(frame, picNum);
if (bufferedImage != null) {
files.put(picNum, bufferedImage);
}
picNum++;
if (count != null && picNum == count) {
break;
}
}
}
second += stepSecond;
if(picNum > 60) {
break;
}
}
ff.stop();
} catch (Exception e) {
log.error("下载抽帧失败,ipPort:{},videoUrl:{},msg:{}", null, videoUrl, e.getMessage());
e.printStackTrace();
}
return files;
}
/**
* 视频文件指定时间段的帧截取
*
* @param videoUrl 视频文件URL
* @param start 视频开始的帧
* @param count 需要截取的帧个数
* @param isAvgTime 在截帧时 是否均匀分布计算时间
* @return
*/
public static Map<Integer, BufferedImage> videoIntercept(String videoUrl, int start, int count, boolean isAvgTime) {
log.info("开始抽取视频帧数,videoUrl:{}",videoUrl);
Frame frame = null;
//<时间, 图片流>
Map<Integer, BufferedImage> files = new HashMap<>();
FFmpegFrameGrabber fFmpegFrameGrabber = new FFmpegFrameGrabber(videoUrl);
fFmpegFrameGrabber.setOption("timeout", "40000000");
try {
fFmpegFrameGrabber.start();
long frameTime = 1;
if (isAvgTime) {
frameTime = fFmpegFrameGrabber.getLengthInTime() / count / 1000000L;
if (frameTime < 0) {
frameTime = 1;
}
}
for (int i = start; i <= count; i++) {
fFmpegFrameGrabber.setTimestamp(i * frameTime * 1000 * 1000);
frame = fFmpegFrameGrabber.grabImage();
BufferedImage bufferedImage = doExecuteFrame(frame, i);
if (bufferedImage != null) {
files.put(i, bufferedImage);
}
}
fFmpegFrameGrabber.stop();
} catch (Exception E) {
log.info("下载的视频抽帧失败,msg:" + E.getMessage());
E.printStackTrace();
}
return files;
}
/*
* @description: BufferedImage 转 inputStream
* @author: wangkanglu
* @date: 2023/12/8 14:52
* @param: [image]
* @return: java.io.InputStream
**/
public static InputStream bufferedImageToInputStream(BufferedImage image) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
InputStream input = new ByteArrayInputStream(os.toByteArray());
return input;
} catch (IOException e) {
}
return null;
}
public static void main(String[] args) throws IOException {
String videoUrl = "http://vd2.bdstatic.com/mda-pej1ztfufz8axvtu/360p/h264/1684545921389774683/mda-pej1ztfufz8axvtu.mp4";
Map<Integer, BufferedImage> integerInputStreamMap = videoUrlIntercept(videoUrl, 1, 13);
System.out.println(integerInputStreamMap.size());
for (Integer seconds : integerInputStreamMap.keySet()) {
BufferedImage bufferedImage = integerInputStreamMap.get(seconds);
String fileName = System.currentTimeMillis()+"抖音测试3" + "_" + seconds + ".jpg";
String filePath = "D:\\Videos\\"+fileName;
//本地磁盘存储
// 本地图片保存地址
ImageIO.write(bufferedImage, "png", new File(filePath));
System.out.println("seconds: " + seconds + ", uploadURL: " + filePath);
}
}
}
附赠压缩图片工具类
java
package com.wkl.testdemo.vedio;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@Slf4j
@Component
public class VideoFrameGrabber {
public static void main(String[] args) throws Exception {
// 视频地址
String vedioUrl = "http://vd2.bdstatic.com/mda-pej1ztfufz8axvtu/360p/h264/1684545921389774683/mda-pej1ztfufz8axvtu.mp4";
BufferedImage image = doExecuteFrameByTime(vedioUrl, 10);
double targetSize = 10*1024;
while (imageToBytes(image).length > targetSize) {
float reduceMultiple = 0.5f;
image = resizeImage(image, reduceMultiple);
}
// 本地图片保存地址
ImageIO.write(image, "png", new File("D:\\Videos\\test6.jpg"));
}
/*
* @description: 读取视频第n秒的图片帧
* @author: wangkanglu
* @date: 2023/12/8 15:05
* @param: [videoUrl, stepSecond]
* @return: java.awt.image.BufferedImage
**/
public static BufferedImage doExecuteFrameByTime(String videoUrl, Integer stepSecond) {
FFmpegFrameGrabber ff = new FFmpegFrameGrabber(videoUrl);
ff.setOption("timeout", "40000000");
long timestamp = stepSecond * 1000000L; //视频是按照微秒计算的,10的6次方分之一秒
try {
ff.start();
ff.setTimestamp(timestamp);
Frame frame = ff.grabImage();
if (frame == null || frame.image == null) {
return null;
}
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage bi = converter.getBufferedImage(frame);
ff.stop();
return bi;
} catch (FrameGrabber.Exception e) {
throw new RuntimeException(e);
}
}
/**
* 通过BufferedImage图片流调整图片大小
* 指定压缩后长宽
*/
public static BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) throws IOException {
Image resultingImage = originalImage.getScaledInstance(targetWidth, targetHeight, Image.SCALE_AREA_AVERAGING);
BufferedImage outputImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
outputImage.getGraphics().drawImage(resultingImage, 0, 0, null);
return outputImage;
}
/**
* 通过BufferedImage图片流调整图片大小
* @param originalImage
* @param reduceMultiple 缩小倍数
* @return
* @throws IOException
*/
public static BufferedImage resizeImage(BufferedImage originalImage, float reduceMultiple) throws IOException {
int width = (int) (originalImage.getWidth() * reduceMultiple);
int height = (int) (originalImage.getHeight() * reduceMultiple);
Image resultingImage = originalImage.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING);
BufferedImage outputImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
outputImage.getGraphics().drawImage(resultingImage, 0, 0, null);
return outputImage;
}
/**
* 压缩图片到指定大小
* @param srcImgData
* @param reduceMultiple 每次压缩比率
* @return
* @throws IOException
*/
public static byte[] resizeImage(byte[] srcImgData, float reduceMultiple) throws IOException {
BufferedImage bi = ImageIO.read(new ByteArrayInputStream(srcImgData));
int width = (int) (bi.getWidth() * reduceMultiple); // 源图宽度
int height = (int) (bi.getHeight() * reduceMultiple); // 源图高度
Image image = bi.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.setColor(Color.RED);
g.drawImage(image, 0, 0, null); // 绘制处理后的图
g.dispose();
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
ImageIO.write(tag, "JPEG", bOut);
return bOut.toByteArray();
}
/**
* BufferedImage图片流转byte[]数组
*/
public static byte[] imageToBytes(BufferedImage bImage) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
ImageIO.write(bImage, "jpg", out);
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
/**
* byte[]数组转BufferedImage图片流
*/
private static BufferedImage bytesToBufferedImage(byte[] ImageByte) {
ByteArrayInputStream in = new ByteArrayInputStream(ImageByte);
BufferedImage image = null;
try {
image = ImageIO.read(in);
} catch (IOException e) {
e.printStackTrace();
}
return image;
}
}