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