zqy
14 小时以前 7c0603315e57e3765270a8ac6b310b5a32af5a40
zhang-content/src/main/java/com/ruoyi/service/impl/VideoProcessService.java
@@ -1,6 +1,7 @@
package com.ruoyi.service.impl;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
@@ -8,13 +9,20 @@
import org.springframework.web.multipart.MultipartFile;
import ws.schild.jave.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
@@ -22,9 +30,6 @@
    @Value("${video.upload-dir:./uploads/videos}")
    private String uploadDir;
    @Value("${video.thumbnail-dir:/profile/thumbnails}")
    private String thumbnailDir;
    @Value("${video.thumbnail.width:320}")
    private int thumbnailWidth;
@@ -38,64 +43,575 @@
    @Value("${video.thumbnail.format:jpg}")
    private String thumbnailFormat;
    @Value("${video.thumbnail.count:1}")
    private int thumbnailCount; // 生成封面数量
    // 支持的视频格式
    private static final Set<String> SUPPORTED_VIDEO_FORMATS =
        new HashSet<>(Arrays.asList("mp4", "avi", "mov", "mkv", "flv", "wmv", "webm", "mpeg", "mpg", "3gp", "m4v"));
    public File convertToFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        File tempFile = File.createTempFile("video_", extension);
        multipartFile.transferTo(tempFile);
        tempFile.deleteOnExit();
        return tempFile;
    }
    /**
     * 处理视频文件:只生成封面
     * 处理图片文件
     */
    public VideoProcessResult processVideo(MultipartFile file) {
        VideoProcessResult result = new VideoProcessResult();
    public Map<String, Object> processImage(File file, int width, int height,
                                            float quality, String format,boolean deleteY) throws IOException {
        Map<String, Object> result = new HashMap<>();
        // 读取图片
        BufferedImage image = ImageIO.read(file);
        if (image == null) {
            result.put("success", false);
            result.put("message", "无法读取图片文件");
            return result;
        }
        int originalWidth = image.getWidth();
        int originalHeight = image.getHeight();
        // 调整尺寸
        if (width > 0 && height > 0) {
            image = resizeImage(image, width, height, true);
        }
        // 压缩并转换为Base64
        byte[] compressedBytes = compressImage(image, quality, format);
        String base64 = Base64.getEncoder().encodeToString(compressedBytes);
        String dataUrl = "data:image/" + format + ";base64," + base64;
        // 返回结果
        result.put("success", true);
        result.put("type", "image");
        result.put("format", format);
        result.put("compressedSize", compressedBytes.length);
        result.put("compressedWidth", width > 0 ? width : originalWidth);
        result.put("compressedHeight", height > 0 ? height : originalHeight);
        result.put("base64", dataUrl);
        result.put("message", "图片压缩成功");
        if (deleteY) {
            file.delete();
            result.put("originalDeleted", false);
            result.put("message", "图片压缩成功");
        }
        return result;
    }
    /**
     * 调整图片大小
     */
    private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth,
                                      int targetHeight, boolean keepAspectRatio) {
        int originalWidth = originalImage.getWidth();
        int originalHeight = originalImage.getHeight();
        // 如果宽高都为0,则不调整
        if (targetWidth <= 0 && targetHeight <= 0) {
            return originalImage;
        }
        int newWidth, newHeight;
        if (keepAspectRatio) {
            // 保持宽高比
            if (targetWidth <= 0) {
                // 只给高度
                double scale = (double) targetHeight / originalHeight;
                newWidth = (int) (originalWidth * scale);
                newHeight = targetHeight;
            } else if (targetHeight <= 0) {
                // 只给宽度
                double scale = (double) targetWidth / originalWidth;
                newWidth = targetWidth;
                newHeight = (int) (originalHeight * scale);
            } else {
                // 两个维度都给了,保持宽高比缩放
                double widthRatio = (double) targetWidth / originalWidth;
                double heightRatio = (double) targetHeight / originalHeight;
                double scale = Math.min(widthRatio, heightRatio);
                newWidth = (int) (originalWidth * scale);
                newHeight = (int) (originalHeight * scale);
            }
        } else {
            // 不保持宽高比
            newWidth = targetWidth > 0 ? targetWidth : originalWidth;
            newHeight = targetHeight > 0 ? targetHeight : originalHeight;
        }
        // 确保最小为1
        newWidth = Math.max(1, newWidth);
        newHeight = Math.max(1, newHeight);
        BufferedImage resizedImage = new BufferedImage(newWidth, newHeight,
            originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType());
        Graphics2D g = resizedImage.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
            RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
        g.dispose();
        return resizedImage;
    }
    /**
     * 压缩图片
     */
    private byte[] compressImage(BufferedImage image, float quality, String format) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 根据格式选择压缩方式
        if ("jpg".equalsIgnoreCase(format) || "jpeg".equalsIgnoreCase(format)) {
            compressAsJpeg(image, baos, quality);
        } else if ("png".equalsIgnoreCase(format)) {
            compressAsPng(image, baos, 6); // PNG压缩级别6
        } else {
            // 其他格式使用默认方式
            ImageIO.write(image, format, baos);
        }
        return baos.toByteArray();
    }
    /**
     * 压缩为JPEG格式
     */
    private void compressAsJpeg(BufferedImage image, ByteArrayOutputStream output, float quality)
        throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
        if (!writers.hasNext()) {
            ImageIO.write(image, "jpg", output);
            return;
        }
        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        // 设置压缩质量
        if (quality > 0) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(Math.max(0.1f, Math.min(1.0f, quality)));
        }
        writer.setOutput(new MemoryCacheImageOutputStream(output));
        writer.write(null, new IIOImage(image, null, null), param);
        writer.dispose();
    }
    /**
     * 压缩为PNG格式
     */
    private void compressAsPng(BufferedImage image, ByteArrayOutputStream output, int compressionLevel)
        throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
        if (!writers.hasNext()) {
            ImageIO.write(image, "png", output);
            return;
        }
        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        // PNG的压缩级别(0-9,0最快但压缩率低,9最慢但压缩率高)
        if (compressionLevel >= 0) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Deflate");
            param.setCompressionQuality(Math.max(0.0f, Math.min(1.0f, compressionLevel / 9.0f)));
        }
        writer.setOutput(new MemoryCacheImageOutputStream(output));
        writer.write(null, new IIOImage(image, null, null), param);
        writer.dispose();
    }
    /**
     * 处理视频文件:生成Base64编码的封面
     */
    public Map<String, Object> processVideo(File file, int width, int height,
                                            float quality, String format,boolean deleteY) {
        Map<String, Object> result = new HashMap<>();
        File tempVideoFile = file;
        System.out.println();
        try {
            // 1. 验证视频格式
            if (!isVideoFile(file)) {
                result.setSuccess(false);
                result.setMessage("不支持的视频格式");
                result.put("success", false);
                result.put("message", "不支持的视频格式");
                return result;
            }
            // 2. 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String fileExtension = getFileExtension(originalFilename);
            String uuid = UUID.randomUUID().toString().replace("-", "");
            String fileName = uuid + fileExtension;
            // 2. 保存原始文件到临时目录
//            String originalFilename = file.getName();
//            tempVideoFile = File.createTempFile("video_", ".temp");
            // 3. 创建目录
            createDirectories();
            // 3. 获取视频信息
//            MultimediaInfo info = getVideoInfo(tempVideoFile);
//            double durationSeconds = info.getDuration() / 1000.0;
//            String videoResolution = info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight();
            // 4. 保存原始文件
            Path originalPath = Paths.get(uploadDir, fileName);
            file.transferTo(originalPath.toFile());
            // 4. 生成Base64格式的封面
            String base64Thumbnail = generateBase64ThumbnailFromFile(
                tempVideoFile,
                width > 0 ? width : thumbnailWidth,
                height > 0 ? height : thumbnailHeight,
                (int) quality,
                format != null ? format : thumbnailFormat
            );
            // 5. 获取视频信息
            MultimediaInfo info = getVideoInfo(originalPath.toFile());
            if (base64Thumbnail == null || base64Thumbnail.isEmpty()) {
                result.put("success", false);
                result.put("message", "封面生成失败");
                return result;
            }
            // 设置结果
            result.setSuccess(true);
            result.setMessage("封面生成成功");
            result.setOriginalPath(originalPath.toString());
            result.setOriginalFilename(originalFilename);
            result.setFileSize(file.getSize());
            result.setDuration(info.getDuration() / 1000.0); // 转换为秒
            result.setResolution(info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight());
            result.setVideoFormat(info.getFormat());
            // 5. 获取缩略图大小
            String thumbnailSize = extractImageSizeFromBase64(base64Thumbnail);
            String[] sizeParts = thumbnailSize.split("x");
            int thumbWidth = sizeParts.length > 0 ? Integer.parseInt(sizeParts[0]) : 0;
            int thumbHeight = sizeParts.length > 1 ? Integer.parseInt(sizeParts[1]) : 0;
            // 6. 生成封面
            String thumbnailPath = generateThumbnail(originalPath.toFile().getPath());
            result.setThumbnailPath(thumbnailPath);
            // 6. 返回结果
            result.put("success", true);
            result.put("type", "video");
            result.put("format", format != null ? format : thumbnailFormat);
//            result.put("originalFilename", originalFilename);
//            result.put("originalWidth", info.getVideo().getSize().getWidth());
//            result.put("originalHeight", info.getVideo().getSize().getHeight());
//            result.put("originalResolution", videoResolution);
//            result.put("duration", durationSeconds);
//            result.put("videoFormat", info.getFormat());
//            result.put("compressedWidth", thumbWidth);
//            result.put("compressedHeight", thumbHeight);
            result.put("base64", base64Thumbnail);
            result.put("message", "视频封面生成成功");
        } catch (Exception e) {
            log.error("视频封面生成失败", e);
            result.setSuccess(false);
            result.setMessage("封面生成失败: " + e.getMessage());
            result.put("success", false);
            result.put("message", "封面生成失败: " + e.getMessage());
        } finally {
            // 清理临时文件
            if (tempVideoFile != null && tempVideoFile.exists() && deleteY) {
                try {
                    tempVideoFile.delete();
                } catch (Exception e) {
                    log.warn("删除临时视频文件时出错", e);
                }
            }
        }
        return result;
    }
    /**
     * 生成Base64格式的封面(可自定义参数)
     */
    private String generateBase64ThumbnailFromFile(File videoFile, int width, int height,
                                                   int quality, String format) {
        File tempThumbnailFile = null;
        Process process = null;
        try {
            if (!videoFile.exists()) {
                log.error("视频文件不存在: {}", videoFile.getAbsolutePath());
                return null;
            }
            // 1. 创建临时文件用于保存缩略图
            String ext = format != null ? format : thumbnailFormat;
            tempThumbnailFile = File.createTempFile("thumb_", "." + ext);
            tempThumbnailFile.deleteOnExit();
            // 2. 获取视频时长,计算截图时间点
            MultimediaInfo info = getVideoInfo(videoFile);
            double durationSeconds = info.getDuration() / 1000.0;
            double screenshotTime = calculateScreenshotTime(durationSeconds);
            // 3. 构建FFmpeg命令
            String ffmpegCmd = String.format(
                "ffmpeg -y -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d -f image2 \"%s\"",
                videoFile.getAbsolutePath(),
                screenshotTime,
                quality,
                width,
                height,
                tempThumbnailFile.getAbsolutePath()
            );
            // 4. 执行FFmpeg命令
            ProcessBuilder processBuilder = new ProcessBuilder();
            String[] command = isWindows() ?
                new String[]{"cmd.exe", "/c", ffmpegCmd} :
                new String[]{"bash", "-c", ffmpegCmd};
            processBuilder.command(command);
            processBuilder.redirectErrorStream(true);
            process = processBuilder.start();
            // 5. 等待命令完成
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return null;
            }
            int exitCode = process.waitFor();
            if (exitCode != 0 || !tempThumbnailFile.exists() || tempThumbnailFile.length() == 0) {
                return null;
            }
            // 6. 读取图片文件并转换为Base64
            byte[] fileContent = Files.readAllBytes(tempThumbnailFile.toPath());
            if (!isValidImage(fileContent)) {
                return null;
            }
            // 7. 转换为Base64
            String base64 = Base64.getEncoder().encodeToString(fileContent);
            String mimeType = getMimeType(ext);
            return "data:" + mimeType + ";base64," + base64;
        } catch (Exception e) {
            log.error("生成Base64封面失败", e);
            return null;
        } finally {
            // 清理资源
            if (process != null && process.isAlive()) {
                process.destroy();
            }
            if (tempThumbnailFile != null && tempThumbnailFile.exists()) {
                try {
                    tempThumbnailFile.delete();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }
    /**
     * 生成Base64格式的封面(不存储文件)
     */
    public String generateBase64ThumbnailFromFile(String videoPath) {
        File videoFile = new File(videoPath);
        if (!videoFile.exists()) {
            log.error("视频文件不存在: {}", videoPath);
            return null;
        }
        return generateBase64ThumbnailFromFile(videoFile);
    }
    /**
     * 生成Base64格式的封面(不存储文件)
     */
    public String generateBase64ThumbnailFromFile(File videoFile) {
        File tempThumbnailFile = null;
        Process process = null;
        try {
            if (!videoFile.exists()) {
                log.error("视频文件不存在: {}", videoFile.getAbsolutePath());
                return null;
            }
            // 1. 创建临时文件用于保存缩略图
            tempThumbnailFile = File.createTempFile("thumb_", "." + thumbnailFormat);
            tempThumbnailFile.deleteOnExit(); // 确保文件会被删除
            // 2. 获取视频时长,计算截图时间点
            MultimediaInfo info = getVideoInfo(videoFile);
            double durationSeconds = info.getDuration() / 1000.0;
            double screenshotTime = calculateScreenshotTime(durationSeconds);
            log.debug("视频截图信息 - 文件名: {}, 时长: {}s, 截图时间点: {}s, 目标尺寸: {}x{}, 质量: {}",
                videoFile.getName(), durationSeconds, screenshotTime,
                thumbnailWidth, thumbnailHeight, thumbnailQuality);
            // 3. 构建FFmpeg命令
            String ffmpegCmd = String.format(
                "ffmpeg -y -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d -f image2 \"%s\"",
                videoFile.getAbsolutePath(),  // 输入文件
                screenshotTime,                // 跳转到的时间点(秒)
                thumbnailQuality,              // 输出质量 (2-31,值越小质量越高)
                thumbnailWidth,                // 输出宽度
                thumbnailHeight,               // 输出高度
                tempThumbnailFile.getAbsolutePath() // 输出文件
            );
            log.debug("执行FFmpeg命令: {}", ffmpegCmd);
            // 4. 执行FFmpeg命令
            ProcessBuilder processBuilder = new ProcessBuilder();
            String[] command = isWindows() ?
                new String[]{"cmd.exe", "/c", ffmpegCmd} :
                new String[]{"bash", "-c", ffmpegCmd};
            processBuilder.command(command);
            processBuilder.redirectErrorStream(true);
            process = processBuilder.start();
            // 5. 读取输出(用于调试)
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }
            // 6. 等待命令完成(带超时)
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                log.error("FFmpeg命令执行超时,视频文件: {}", videoFile.getName());
                return null;
            }
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                log.error("FFmpeg命令执行失败,退出码: {}, 视频文件: {}, 输出: {}",
                    exitCode, videoFile.getName(), output.toString());
                return null;
            }
            // 7. 检查生成的图片文件
            if (!tempThumbnailFile.exists() || tempThumbnailFile.length() == 0) {
                log.error("生成的缩略图为空或不存在,视频文件: {}", videoFile.getName());
                return null;
            }
            // 8. 读取图片文件并转换为Base64
            byte[] fileContent = Files.readAllBytes(tempThumbnailFile.toPath());
            // 9. 验证图片有效性
            if (!isValidImage(fileContent)) {
                log.error("生成的缩略图不是有效的图片,视频文件: {}", videoFile.getName());
                return null;
            }
            // 10. 转换为Base64
            String base64 = Base64.getEncoder().encodeToString(fileContent);
            String mimeType = getMimeType(thumbnailFormat);
            return "data:" + mimeType + ";base64," + base64;
        } catch (Exception e) {
            log.error("生成Base64封面失败,视频文件: {}", videoFile.getName(), e);
            return null;
        } finally {
            // 清理资源
            if (process != null && process.isAlive()) {
                process.destroy();
            }
            // 删除临时文件
            if (tempThumbnailFile != null && tempThumbnailFile.exists()) {
                try {
                    boolean deleted = tempThumbnailFile.delete();
                    if (deleted) {
                        log.debug("已删除临时缩略图文件: {}", tempThumbnailFile.getAbsolutePath());
                    } else {
                        log.warn("无法删除临时缩略图文件: {}", tempThumbnailFile.getAbsolutePath());
                    }
                } catch (Exception e) {
                    log.warn("删除临时缩略图文件时出错", e);
                }
            }
        }
    }
    /**
     * 从Base64字符串中提取图片尺寸
     */
    private String extractImageSizeFromBase64(String base64) {
        try {
            // Base64字符串格式: data:image/jpeg;base64,/9j/4AAQSkZJRgABA...
            String base64Data = base64.split(",")[1];
            byte[] imageBytes = Base64.getDecoder().decode(base64Data);
            try (ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes)) {
                BufferedImage image = ImageIO.read(bis);
                if (image != null) {
                    return image.getWidth() + "x" + image.getHeight();
                }
            }
        } catch (Exception e) {
            log.warn("无法从Base64中提取图片尺寸", e);
        }
        return thumbnailWidth + "x" + thumbnailHeight; // 返回目标尺寸
    }
    /**
     * 计算截图时间点
     */
    private double calculateScreenshotTime(double durationSeconds) {
        if (durationSeconds <= 0) {
            return 0;
        }
        // 如果视频长度小于1秒,在中间截图
        if (durationSeconds < 1) {
            return durationSeconds / 2;
        }
        // 如果视频长度小于3秒,在第1秒截图
        if (durationSeconds < 3) {
            return 1.0;
        }
        // 视频较长时,在10%的位置截图(但不超过30秒)
        double time = durationSeconds * 0.1;
        return Math.min(time, 30.0);
    }
    /**
     * 获取视频信息
     */
    private MultimediaInfo getVideoInfo(File videoFile) throws Exception {
        try {
            MultimediaObject multimediaObject = new MultimediaObject(videoFile);
            return multimediaObject.getInfo();
        } catch (Exception e) {
            log.error("获取视频信息失败: {}", videoFile.getAbsolutePath(), e);
            throw new Exception("无法获取视频信息: " + e.getMessage());
        }
    }
    /**
@@ -129,194 +645,46 @@
    }
    /**
     * 生成封面(缩略图)
     * 批量生成Base64格式的缩略图
     */
    public String generateThumbnail(String videoPath) throws Exception {
        File videoFile = new File(RuoYiConfig.getProfile() + videoPath.replace("/profile", ""));
        if (!videoFile.exists()) {
            throw new FileNotFoundException("视频文件不存在: " + videoPath);
        }
        // 生成唯一的缩略图文件名(与视频同目录)
        String originalName = videoFile.getName();
        String baseName = getFileNameWithoutExtension(originalName);
        String thumbnailName = "thumb_" + baseName + "_" +
            UUID.randomUUID().toString().substring(0, 8) +
            "." + thumbnailFormat;
        // *** 关键修改:缩略图生成在视频文件所在目录 ***
        // 获取视频文件所在的目录
        File videoDir = videoFile.getParentFile();
        if (videoDir == null) {
            throw new RuntimeException("无法获取视频文件所在目录: " + videoFile.getAbsolutePath());
        }
        // 确保视频目录存在
        if (!videoDir.exists()) {
            videoDir.mkdirs();
        }
        // 创建缩略图文件对象(在视频同目录)
        File thumbnailFile = new File(videoDir, thumbnailName);
        System.out.println("------------------------缩略图文件名: " + thumbnailName);
        System.out.println("------------------------缩略图完整路径: " + thumbnailFile.getAbsolutePath());
        System.out.println("------------------------视频文件目录: " + videoDir.getAbsolutePath());
        // 1. 获取视频时长,计算截图时间点
        MultimediaInfo info = getVideoInfo(videoFile);
        double durationSeconds = info.getDuration() / 1000.0;
        double screenshotTime = calculateScreenshotTime(durationSeconds);
        System.out.println("------------------------截图时间点: " + screenshotTime);
        // 2. 构建并执行 FFmpeg 命令
        String ffmpegCmd = String.format(
            "ffmpeg -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d \"%s\"",
            videoFile.getAbsolutePath(),  // 输入文件
            screenshotTime,               // 跳转到的时间点(秒)
            thumbnailQuality,             // 输出质量 (2-31,值越小质量越高)
            thumbnailWidth,               // 输出宽度
            thumbnailHeight,              // 输出高度
            thumbnailFile.getAbsolutePath() // 输出文件(与视频同目录)
        );
        Process process = Runtime.getRuntime().exec(ffmpegCmd);
        // 3. 读取并打印命令输出(用于调试)
        try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
             BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = errorReader.readLine()) != null) {
                System.out.println("[FFmpeg Error] " + line);
            }
            while ((line = inputReader.readLine()) != null) {
                System.out.println("[FFmpeg Output] " + line);
            }
        }
        int exitCode = process.waitFor();
        System.out.println("------------------------FFmpeg 退出码: " + exitCode);
        if (exitCode != 0) {
            throw new RuntimeException("FFmpeg 截图失败,退出码: " + exitCode);
        }
        if (!thumbnailFile.exists() || thumbnailFile.length() == 0) {
            throw new RuntimeException("生成的缩略图为空或不存在");
        }
        // 获取缩略图的绝对路径
        String absolutePath = thumbnailFile.getAbsolutePath();
        // 将绝对路径转换为相对profilePath的相对路径
        String relativePath = absolutePath.substring(RuoYiConfig.getProfile().length()).replace("\\", "/");
        // 确保相对路径以 / 开头
        if (!relativePath.startsWith("/")) {
            relativePath = "/" + relativePath;
        }
        System.out.println("------------------------生成的相对路径: " + relativePath);
        return "/profile"+relativePath;
    }
    /**
     * 计算截图时间点
     */
    private double calculateScreenshotTime(double durationSeconds) {
        // 1. 尝试在第1秒
        // 2. 如果视频不足1秒,在中间截图
        // 3. 如果视频很短(<0.5秒),在开头截图
        double screenshotTime = 1.0; // 默认第1秒
        if (durationSeconds < 1.0) {
            screenshotTime = durationSeconds / 2; // 中间
        }
        if (durationSeconds < 0.5) {
            screenshotTime = 0; // 开头
        }
        return screenshotTime;
    }
    /**
     * 获取视频信息
     */
    private MultimediaInfo getVideoInfo(File videoFile) throws Exception {
        MultimediaObject multimediaObject = new MultimediaObject(videoFile);
        return multimediaObject.getInfo();
    }
    /**
     * 批量生成缩略图
     */
    public BatchThumbnailResult batchGenerateThumbnails(List<MultipartFile> files) {
        BatchThumbnailResult result = new BatchThumbnailResult();
        List<VideoProcessResult> results = new ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (MultipartFile file : files) {
            try {
                VideoProcessResult singleResult = processVideo(file);
                results.add(singleResult);
                if (singleResult.isSuccess()) {
                    successCount++;
                } else {
                    failCount++;
                }
            } catch (Exception e) {
                log.error("处理视频失败: {}", file.getOriginalFilename(), e);
                failCount++;
            }
        }
        result.setResults(results);
        result.setTotal(files.size());
        result.setSuccess(successCount);
        result.setFail(failCount);
        return result;
    }
    /**
     * 从视频目录批量生成缩略图
     */
    public void generateThumbnailsFromDirectory(File videoDir) throws Exception {
        if (!videoDir.exists() || !videoDir.isDirectory()) {
            throw new IllegalArgumentException("视频目录不存在");
        }
        File[] videoFiles = videoDir.listFiles((dir, name) -> isVideoFile(new File(dir, name)));
        if (videoFiles == null || videoFiles.length == 0) {
            log.warn("视频目录中没有找到支持的视频文件");
            return;
        }
        log.info("开始批量生成缩略图,共{}个视频", videoFiles.length);
        int successCount = 0;
        int failCount = 0;
        for (File videoFile : videoFiles) {
            try {
                generateThumbnail(videoFile.getPath());
                successCount++;
                log.info("已生成缩略图: {}", videoFile.getName());
            } catch (Exception e) {
                failCount++;
                log.error("生成缩略图失败: {}", videoFile.getName(), e);
            }
        }
        log.info("批量生成缩略图完成,成功{}个,失败{}个", successCount, failCount);
    }
//    public BatchThumbnailResult batchGenerateThumbnails(List<MultipartFile> files) {
//        BatchThumbnailResult result = new BatchThumbnailResult();
//        List<VideoProcessResult> results = new ArrayList<>();
//
//        int successCount = 0;
//        int failCount = 0;
//
//        for (MultipartFile file : files) {
//            try {
//                VideoProcessResult singleResult = processVideo(file);
//                results.add(singleResult);
//
//                if (singleResult.isSuccess()) {
//                    successCount++;
//                } else {
//                    failCount++;
//                }
//
//            } catch (Exception e) {
//                log.error("处理视频失败: {}", file.getOriginalFilename(), e);
//
//                VideoProcessResult errorResult = new VideoProcessResult();
//                errorResult.setSuccess(false);
//                errorResult.setMessage("处理失败: " + e.getMessage());
//                errorResult.setOriginalFilename(file.getOriginalFilename());
//                results.add(errorResult);
//
//                failCount++;
//            }
//        }
//
//        result.setResults(results);
//        result.setTotal(files.size());
//        result.setSuccess(successCount);
//        result.setFail(failCount);
//
//        return result;
//    }
    /**
     * 获取视频时长
@@ -361,7 +729,7 @@
        if (fileName == null) return "";
        int lastDot = fileName.lastIndexOf('.');
        if (lastDot > 0 && lastDot < fileName.length() - 1) {
            return fileName.substring(lastDot + 1);
            return fileName.substring(lastDot + 1).toLowerCase();
        }
        return "";
    }
@@ -379,63 +747,58 @@
    }
    /**
     * 根据图片格式获取编码器
     * 验证图片是否有效
     */
    private String getCodecForFormat(String format) {
    private boolean isValidImage(byte[] imageData) {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(imageData)) {
            BufferedImage image = ImageIO.read(bis);
            return image != null;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 获取MIME类型
     */
    private String getMimeType(String format) {
        switch (format.toLowerCase()) {
            case "jpg":
            case "jpeg":
                return "mjpeg";
                return "image/jpeg";
            case "png":
                return "png";
                return "image/png";
            case "gif":
                return "gif";
                return "image/gif";
            case "bmp":
                return "bmp";
                return "image/bmp";
            case "webp":
                return "image/webp";
            default:
                return "png";
                return "image/jpeg";
        }
    }
    /**
     * 根据扩展名获取格式
     * 判断操作系统是否是Windows
     */
    private String getFormatForExtension(String extension) {
        switch (extension.toLowerCase()) {
            case "jpg":
            case "jpeg":
                return "image2";
            case "png":
                return "image2";
            case "gif":
                return "gif";
            case "bmp":
                return "bmp";
            default:
                return "image2";
        }
    }
    /**
     * 创建目录
     */
    private void createDirectories() throws IOException {
        Files.createDirectories(Paths.get(uploadDir));
        Files.createDirectories(Paths.get(thumbnailDir));
    private boolean isWindows() {
        String os = System.getProperty("os.name").toLowerCase();
        return os.contains("win");
    }
    @Data
    public static class VideoProcessResult {
        private boolean success;
        private String message;
        private String originalPath;
        private String originalFilename;
        private String thumbnailPath;
        private List<String> thumbnailPaths; // 多个封面路径
        private String thumbnailBase64;  // Base64编码的封面
        private String mimeType;         // 封面MIME类型
        private long fileSize;          // 原始文件大小
        private double duration;        // 视频时长(秒)
        private String resolution;      // 视频分辨率
        private String videoFormat;     // 视频格式
        private String thumbnailSize;   // 缩略图尺寸
        public String getFileSizeFormatted() {
            return formatFileSize(fileSize);
@@ -511,4 +874,3 @@
        }
    }
}