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 processVideoToHex(File videoFile, int frameRate, int duration, int width, int height) throws Exception { // 1. 提取视频帧 List frames = extractFramesFromVideo(videoFile, frameRate, duration, width, height); // 2. 转换为RGB565格式 byte[] binaryData = convertFramesToRGB565(frames); // 3. 转换为Hex字符串列表 return bytesToHexList(binaryData); } /** * 从视频中提取帧 */ private List extractFramesFromVideo(File videoFile, int frameRate, int duration, int width, int height) throws Exception { List frames = new ArrayList<>(); int totalFramesToExtract = frameRate * duration; try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(videoFile)) { grabber.start(); long totalFramesInVideo = grabber.getLengthInFrames(); int fps = (int) Math.round(grabber.getFrameRate()); if (fps <= 0) fps = 30; double durationSeconds = (double) totalFramesInVideo / fps; if (durationSeconds < duration) { throw new IllegalArgumentException("视频太短,至少需要 " + duration + " 秒"); } double frameInterval = (double) totalFramesInVideo / totalFramesToExtract; for (int i = 0; i < totalFramesToExtract; i++) { int targetFrameNumber = (int) Math.round(i * frameInterval); if (targetFrameNumber >= totalFramesInVideo) { throw new IllegalArgumentException("目标帧超出范围: " + targetFrameNumber); } grabber.setFrameNumber(targetFrameNumber); Frame frame = grabber.grab(); if (frame != null && frame.image != null) { BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame); frames.add(cropImage(bufferedImage, width, height)); } else { throw new IllegalArgumentException("无法获取第 " + targetFrameNumber + "帧"); } } grabber.stop(); } log.debug("从视频中提取了 {} 帧", frames.size()); return frames; } /** * 将所有帧转换为 RGB565 格式字节数组 */ private byte[] convertFramesToRGB565(List 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 bytesToHexList(byte[] bytes) { List 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 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); } } } }