From 36092932bf5cb57614727066bb3e62d21882284b Mon Sep 17 00:00:00 2001 From: DragonWenLong <552045633@qq.com> Date: Fri, 18 Jul 2025 13:27:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(fys-demo):=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E4=B8=8A=E4=BC=A0=E5=92=8C=E5=A4=84=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 VideoUploadController 控制器处理视频上传 - 添加视频帧提取和转换为 RGB565 格式功能 - 实现视频文件大小和格式校验 - 优化临时文件创建和清理逻辑 - 引入 javacv-platform依赖进行视频处理 --- fys-modules/fys-demo/pom.xml | 5 + .../controller/VideoUploadController.java | 244 ++++++++++++++++++ pom.xml | 8 + 3 files changed, 257 insertions(+) create mode 100644 fys-modules/fys-demo/src/main/java/com/fuyuanshen/demo/controller/VideoUploadController.java diff --git a/fys-modules/fys-demo/pom.xml b/fys-modules/fys-demo/pom.xml index d53f11b..8b0c411 100644 --- a/fys-modules/fys-demo/pom.xml +++ b/fys-modules/fys-demo/pom.xml @@ -103,6 +103,11 @@ fys-common-websocket + + org.bytedeco + javacv-platform + + diff --git a/fys-modules/fys-demo/src/main/java/com/fuyuanshen/demo/controller/VideoUploadController.java b/fys-modules/fys-demo/src/main/java/com/fuyuanshen/demo/controller/VideoUploadController.java new file mode 100644 index 0000000..ddbaeff --- /dev/null +++ b/fys-modules/fys-demo/src/main/java/com/fuyuanshen/demo/controller/VideoUploadController.java @@ -0,0 +1,244 @@ +package com.fuyuanshen.demo.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.fuyuanshen.common.core.domain.R; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +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.Base64; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.Java2DFrameUtils; +import org.springframework.http.ResponseEntity; + +@RestController +@RequestMapping("/video") +public class VideoUploadController { + + // 可配置项:建议从 application.yml 中读取 + private static final int MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10 MB + private static final int FRAME_RATE = 15; // 每秒抽15帧 + private static final int DURATION = 2; // 抽2秒 + private static final int TOTAL_FRAMES = FRAME_RATE * DURATION; + private static final int WIDTH = 160; + private static final int HEIGHT = 80; + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @SaIgnore + public R> upload(@RequestParam("file") MultipartFile file) { + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + + if (!isVideo(file.getOriginalFilename())) { + return R.fail("只允许上传视频文件"); + } + + if (file.getSize() > MAX_VIDEO_SIZE) { + return R.fail("视频大小不能超过10MB"); + } + + File tempFile = null; + try { + // 创建临时文件保存上传的视频 + tempFile = createTempVideoFile(file); + + + List frames = extractFramesFromVideo(tempFile); + if (frames.isEmpty()) { + return R.fail("无法提取任何帧"); + } + + // ✅ 新增:保存帧为图片 + //saveFramesToLocal(frames, "extracted_frame"); + + byte[] binaryData = convertFramesToRGB565(frames); +// String base64Data = Base64.getEncoder().encodeToString(binaryData); +// +// return R.ok(base64Data); + // 构造响应头 + // 将二进制数据转为 Hex 字符串 + // 转换为 Hex 字符串列表 + List hexList = bytesToHexList(binaryData); + + return R.ok(hexList); + + } catch (Exception e) { + return R.fail("视频处理失败:" + e.getMessage()); + } finally { + deleteTempFile(tempFile); + } + } + + /** + * rgb565 转 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; + } + + /** + * 创建临时文件并保存上传的视频 + */ + private File createTempVideoFile(MultipartFile file) throws Exception { + File tempFile = Files.createTempFile("upload-", ".mp4").toFile(); + file.transferTo(tempFile); + return tempFile; + } + + /** + * 从视频中按时间均匀提取指定数量的帧 + */ + private List extractFramesFromVideo(File videoFile) throws Exception { + List frames = new ArrayList<>(); + + 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 / TOTAL_FRAMES; + + for (int i = 0; i < TOTAL_FRAMES; 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(); + } + + return frames; + } + + /** + * 将抽取的帧保存到本地,用于调试 + */ + private 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); + System.out.println("保存帧图片成功: " + outputImage.getAbsolutePath()); + } catch (Exception e) { + throw new IllegalArgumentException("保存帧图片失败 " + e); + } + } + } + + /** + * 将所有帧转换为 RGB565 格式字节数组 + */ + private byte[] convertFramesToRGB565(List frames) throws Exception { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + for (BufferedImage image : frames) { + byte[] rgb565Bytes = convertToRGB565(image); + byteArrayOutputStream.write(rgb565Bytes); + } + + return byteArrayOutputStream.toByteArray(); + } + + /** + * 判断是否是支持的视频格式 + */ + private boolean isVideo(String filename) { + String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); + return Arrays.asList(".mp4", ".avi", ".mov", ".mkv").contains(ext); + } + + /** + * 裁剪图像到目标尺寸 + */ + 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]; // RGB565: 2 bytes per pixel + 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); // High byte first + result[index++] = (byte) pixel; + } + } + + return result; + } + + /** + * 删除临时文件 + */ + private void deleteTempFile(File file) { + if (file != null && file.exists()) { + if (!file.delete()) { + throw new IllegalArgumentException("无法删除临时文件: " + file.getAbsolutePath()); + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8cb9b27..67f04f5 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,8 @@ 1.18.36 1.80 1.16.7 + + 1.5.7 2.7.0 @@ -142,6 +144,12 @@ ${justauth.version} + + org.bytedeco + javacv-platform + ${javacv-platform.version} + + com.fuyuanshen