Files
fys-Multi-tenant/fys-admin/src/main/java/com/fuyuanshen/web/util/VideoProcessUtil.java

224 lines
7.8 KiB
Java
Raw Normal View History

package com.fuyuanshen.web.util;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
/**
* 视频处理工具类
*/
@Slf4j
@Component
public class VideoProcessUtil {
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
/**
* 创建临时视频文件
*/
public File createTempVideoFile(MultipartFile file) throws Exception {
File tempFile = Files.createTempFile("upload-", ".mp4").toFile();
file.transferTo(tempFile);
log.debug("创建临时视频文件: {}", tempFile.getAbsolutePath());
return tempFile;
}
/**
* 处理视频并转换为Hex字符串列表
*/
public List<String> processVideoToHex(File videoFile, int frameRate, int duration, int width, int height) throws Exception {
// 1. 提取视频帧
List<BufferedImage> frames = extractFramesFromVideo(videoFile, frameRate, duration, width, height);
// 2. 转换为RGB565格式
byte[] binaryData = convertFramesToRGB565(frames);
// 3. 转换为Hex字符串列表
return bytesToHexList(binaryData);
}
2025-11-21 16:24:07 +08:00
/**
* 从视频中提取帧
2025-11-21 16:24:07 +08:00
*
* @param videoFile 视频文件对象
* @param frameRate 每秒提取的帧数帧率
* @param duration 需要提取的视频时长
* @param width 提取帧的宽度
* @param height 提取帧的高度
* @return 提取的帧图像列表
* @throws Exception 如果在提取过程中发生错误
*/
private List<BufferedImage> extractFramesFromVideo(File videoFile, int frameRate, int duration, int width, int height) throws Exception {
2025-11-21 16:24:07 +08:00
// 初始化帧列表
List<BufferedImage> frames = new ArrayList<>();
2025-11-21 16:24:07 +08:00
// 计算需要提取的总帧数 = 帧率 × 时长
int totalFramesToExtract = frameRate * duration;
2025-11-21 16:24:07 +08:00
// 使用FFmpegFrameGrabber从视频文件中抓取帧
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(videoFile)) {
2025-11-21 16:24:07 +08:00
// 启动抓取器
grabber.start();
2025-11-21 16:24:07 +08:00
// 获取视频总帧数
long totalFramesInVideo = grabber.getLengthInFrames();
2025-11-21 16:24:07 +08:00
// 获取视频帧率如果获取不到则默认为30fps
int fps = (int) Math.round(grabber.getFrameRate());
if (fps <= 0) fps = 30;
2025-11-21 16:24:07 +08:00
// 计算视频总时长(秒)
double durationSeconds = (double) totalFramesInVideo / fps;
2025-11-21 16:24:07 +08:00
// 检查视频时长是否满足要求
if (durationSeconds < duration) {
throw new IllegalArgumentException("视频太短,至少需要 " + duration + "");
}
2025-11-21 16:24:07 +08:00
// 计算帧间隔,用于均匀分布提取的帧
double frameInterval = (double) totalFramesInVideo / totalFramesToExtract;
2025-11-21 16:24:07 +08:00
// 循环提取指定数量的帧
for (int i = 0; i < totalFramesToExtract; i++) {
2025-11-21 16:24:07 +08:00
// 计算目标帧号
int targetFrameNumber = (int) Math.round(i * frameInterval);
2025-11-21 16:24:07 +08:00
// 检查目标帧号是否超出视频范围
if (targetFrameNumber >= totalFramesInVideo) {
throw new IllegalArgumentException("目标帧超出范围: " + targetFrameNumber);
}
2025-11-21 16:24:07 +08:00
// 设置抓取器到目标帧
grabber.setFrameNumber(targetFrameNumber);
2025-11-21 16:24:07 +08:00
// 抓取当前帧
Frame frame = grabber.grab();
2025-11-21 16:24:07 +08:00
// 如果成功抓取到帧且帧图像不为空
if (frame != null && frame.image != null) {
2025-11-21 16:24:07 +08:00
// 将帧转换为BufferedImage并裁剪到指定尺寸
BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame);
frames.add(cropImage(bufferedImage, width, height));
} else {
2025-11-21 16:24:07 +08:00
// 如果无法获取帧则抛出异常
throw new IllegalArgumentException("无法获取第 " + targetFrameNumber + "");
}
}
2025-11-21 16:24:07 +08:00
// 停止抓取器
grabber.stop();
}
2025-11-21 16:24:07 +08:00
// 记录提取的帧数
log.debug("从视频中提取了 {} 帧", frames.size());
2025-11-21 16:24:07 +08:00
// 返回提取的帧列表
return frames;
}
2025-11-21 16:24:07 +08:00
/**
* 将所有帧转换为 RGB565 格式字节数组
*/
private byte[] convertFramesToRGB565(List<BufferedImage> frames) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
for (BufferedImage image : frames) {
byte[] rgb565Bytes = convertToRGB565(image);
byteArrayOutputStream.write(rgb565Bytes);
}
byte[] result = byteArrayOutputStream.toByteArray();
log.debug("转换RGB565数据完成总字节数: {}", result.length);
return result;
}
/**
* 将字节数组转换为Hex字符串列表
*/
private List<String> bytesToHexList(byte[] bytes) {
List<String> hexList = new ArrayList<>();
for (byte b : bytes) {
int value = b & 0xFF;
char high = HEX_ARRAY[value >>> 4];
char low = HEX_ARRAY[value & 0x0F];
hexList.add(String.valueOf(high) + low);
}
return hexList;
}
/**
* 删除临时文件
*/
public void deleteTempFile(File file) {
if (file != null && file.exists()) {
if (file.delete()) {
log.debug("删除临时文件成功: {}", file.getAbsolutePath());
} else {
log.warn("无法删除临时文件: {}", file.getAbsolutePath());
}
}
}
/**
* 裁剪图像到目标尺寸
*/
private BufferedImage cropImage(BufferedImage img, int targetWidth, int targetHeight) {
int w = Math.min(img.getWidth(), targetWidth);
int h = Math.min(img.getHeight(), targetHeight);
return img.getSubimage(0, 0, w, h);
}
/**
* BufferedImage 转换为 RGB565 格式的字节数组
*/
private byte[] convertToRGB565(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
byte[] result = new byte[width * height * 2];
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
int r = ((rgb >> 16) & 0xFF) >> 3;
int g = ((rgb >> 8) & 0xFF) >> 2;
int b = (rgb & 0xFF) >> 3;
short pixel = (short) ((r << 11) | (g << 5) | b);
result[index++] = (byte) (pixel >> 8);
result[index++] = (byte) pixel;
}
}
return result;
}
/**
* 保存帧到本地用于调试
*/
public void saveFramesToLocal(List<BufferedImage> frames, String prefix) {
File outputDir = new File("output_frames");
if (!outputDir.exists()) {
outputDir.mkdirs();
}
int index = 0;
for (BufferedImage frame : frames) {
try {
File outputImage = new File(outputDir, prefix + "_" + (index++) + ".png");
ImageIO.write(frame, "png", outputImage);
log.debug("保存帧图片成功: {}", outputImage.getAbsolutePath());
} catch (Exception e) {
log.error("保存帧图片失败", e);
}
}
}
}