| | |
| | | import java.util.List; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import org.bytedeco.javacv.FFmpegFrameGrabber; |
| | | import org.bytedeco.javacv.Java2DFrameConverter; |
| | | import org.bytedeco.javacv.Frame; |
| | | import javax.imageio.ImageIO; |
| | | import java.awt.image.BufferedImage; |
| | | |
| | | |
| | | @Service |
| | | @Slf4j |
| | | public class VideoProcessService { |
| | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * 处理视频文件:生成Base64编码的封面 |
| | | * 处理视频文件:生成Base64编码的封面 (使用JavaCV) |
| | | */ |
| | | public Map<String, Object> processVideo(File file, int width, int height, |
| | | float quality, String format,boolean deleteY) { |
| | | float quality, String format, boolean deleteY) { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | File tempVideoFile = file; |
| | | |
| | | |
| | | System.out.println(); |
| | | |
| | | try { |
| | | // 1. 验证视频格式 |
| | |
| | | return result; |
| | | } |
| | | |
| | | // 2. 保存原始文件到临时目录 |
| | | // String originalFilename = file.getName(); |
| | | // tempVideoFile = File.createTempFile("video_", ".temp"); |
| | | |
| | | // 3. 获取视频信息 |
| | | // MultimediaInfo info = getVideoInfo(tempVideoFile); |
| | | // double durationSeconds = info.getDuration() / 1000.0; |
| | | // String videoResolution = info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight(); |
| | | |
| | | // 4. 生成Base64格式的封面 |
| | | String base64Thumbnail = generateBase64ThumbnailFromFile( |
| | | tempVideoFile, |
| | | // 2. 使用新的JavaCV方法生成Base64封面 |
| | | String base64Thumbnail = generateBase64ThumbnailWithJavaCV( |
| | | file, |
| | | width > 0 ? width : thumbnailWidth, |
| | | height > 0 ? height : thumbnailHeight, |
| | | (int) quality, |
| | | quality, // 注意:这里传入float类型的quality |
| | | format != null ? format : thumbnailFormat |
| | | ); |
| | | |
| | |
| | | return result; |
| | | } |
| | | |
| | | // 5. 获取缩略图大小 |
| | | // 3. 获取缩略图大小 (方法保持不变,依赖Base64) |
| | | 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. 返回结果 |
| | | // 4. 返回结果 |
| | | 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("thumbnailSize", thumbnailSize); // 可选,返回尺寸信息 |
| | | result.put("message", "视频封面生成成功"); |
| | | |
| | | } catch (Exception e) { |
| | |
| | | result.put("message", "封面生成失败: " + e.getMessage()); |
| | | } finally { |
| | | // 清理临时文件 |
| | | if (tempVideoFile != null && tempVideoFile.exists() && deleteY) { |
| | | if (deleteY && file != null && file.exists()) { |
| | | try { |
| | | tempVideoFile.delete(); |
| | | file.delete(); |
| | | } catch (Exception e) { |
| | | log.warn("删除临时视频文件时出错", e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 生成Base64格式的封面(可自定义参数) |
| | | * 【核心】使用JavaCV生成Base64格式的封面 |
| | | */ |
| | | private String generateBase64ThumbnailFromFile(File videoFile, int width, int height, |
| | | int quality, String format) { |
| | | File tempThumbnailFile = null; |
| | | Process process = null; |
| | | /** |
| | | * 【核心】使用JavaCV生成Base64格式的封面 |
| | | */ |
| | | private String generateBase64ThumbnailWithJavaCV(File videoFile, int targetWidth, |
| | | int targetHeight, float quality, |
| | | String format) { |
| | | FFmpegFrameGrabber grabber = null; |
| | | Java2DFrameConverter converter = null; |
| | | |
| | | try { |
| | | if (!videoFile.exists()) { |
| | |
| | | return null; |
| | | } |
| | | |
| | | // 1. 创建临时文件用于保存缩略图 |
| | | String ext = format != null ? format : thumbnailFormat; |
| | | tempThumbnailFile = File.createTempFile("thumb_", "." + ext); |
| | | tempThumbnailFile.deleteOnExit(); |
| | | // 1. 初始化抓取器 |
| | | grabber = new FFmpegFrameGrabber(videoFile); |
| | | grabber.start(); |
| | | |
| | | // 2. 获取视频时长,计算截图时间点 |
| | | MultimediaInfo info = getVideoInfo(videoFile); |
| | | double durationSeconds = info.getDuration() / 1000.0; |
| | | // 获取视频时长 |
| | | long durationMicroseconds = grabber.getLengthInTime(); |
| | | double durationSeconds = durationMicroseconds / 1_000_000.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() |
| | | ); |
| | | log.info("视频信息 - 文件名: {}, 时长: {}s, 截图时间: {}s, 原始尺寸: {}x{}", |
| | | videoFile.getName(), durationSeconds, screenshotTime, |
| | | grabber.getImageWidth(), grabber.getImageHeight()); |
| | | |
| | | // 4. 执行FFmpeg命令 |
| | | ProcessBuilder processBuilder = new ProcessBuilder(); |
| | | String[] command = isWindows() ? |
| | | new String[]{"cmd.exe", "/c", ffmpegCmd} : |
| | | new String[]{"bash", "-c", ffmpegCmd}; |
| | | // 2. 跳转到指定时间点 |
| | | grabber.setTimestamp((long) (screenshotTime * 1_000_000)); |
| | | |
| | | processBuilder.command(command); |
| | | processBuilder.redirectErrorStream(true); |
| | | process = processBuilder.start(); |
| | | // 3. 抓取帧 |
| | | Frame frame = null; |
| | | BufferedImage bufferedImage = null; |
| | | converter = new Java2DFrameConverter(); |
| | | |
| | | // 5. 等待命令完成 |
| | | boolean finished = process.waitFor(30, TimeUnit.SECONDS); |
| | | if (!finished) { |
| | | process.destroyForcibly(); |
| | | // 尝试抓取多帧 |
| | | for (int i = 0; i < 10; i++) { |
| | | frame = grabber.grabImage(); |
| | | if (frame != null && frame.image != null) { |
| | | bufferedImage = converter.convert(frame); |
| | | if (bufferedImage != null) { |
| | | log.info("成功抓取第{}帧,图片尺寸: {}x{}", |
| | | i, bufferedImage.getWidth(), bufferedImage.getHeight()); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (bufferedImage == null) { |
| | | log.error("无法从视频中抓取有效图像帧"); |
| | | return null; |
| | | } |
| | | |
| | | int exitCode = process.waitFor(); |
| | | if (exitCode != 0 || !tempThumbnailFile.exists() || tempThumbnailFile.length() == 0) { |
| | | // 4. 调整图片尺寸 |
| | | int originalWidth = bufferedImage.getWidth(); |
| | | int originalHeight = bufferedImage.getHeight(); |
| | | |
| | | if (targetWidth > 0 || targetHeight > 0) { |
| | | bufferedImage = resizeImage(bufferedImage, targetWidth, targetHeight, true); |
| | | log.info("调整尺寸: {}x{} -> {}x{}", |
| | | originalWidth, originalHeight, |
| | | bufferedImage.getWidth(), bufferedImage.getHeight()); |
| | | } |
| | | |
| | | // 5. 压缩并转换为Base64 |
| | | byte[] compressedBytes; |
| | | String actualFormat = (format != null && !format.isEmpty()) ? format : "jpg"; |
| | | ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| | | |
| | | try { |
| | | if ("jpg".equalsIgnoreCase(actualFormat) || "jpeg".equalsIgnoreCase(actualFormat)) { |
| | | compressAsJpeg(bufferedImage, baos, quality); |
| | | } else if ("png".equalsIgnoreCase(actualFormat)) { |
| | | compressAsPng(bufferedImage, baos, (int)(quality * 9)); |
| | | } else { |
| | | if (!ImageIO.write(bufferedImage, actualFormat, baos)) { |
| | | // 如果不支持该格式,回退到jpg |
| | | log.warn("不支持格式: {},回退到jpg", actualFormat); |
| | | actualFormat = "jpg"; |
| | | baos.reset(); |
| | | compressAsJpeg(bufferedImage, baos, quality); |
| | | } |
| | | } |
| | | |
| | | compressedBytes = baos.toByteArray(); |
| | | |
| | | if (compressedBytes == null || compressedBytes.length == 0) { |
| | | log.error("压缩后的图片数据为空"); |
| | | return null; |
| | | } |
| | | |
| | | log.info("图片压缩成功,大小: {} bytes", compressedBytes.length); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("图片压缩失败: {}", e.getMessage()); |
| | | // 尝试使用默认方式 |
| | | baos.reset(); |
| | | if (ImageIO.write(bufferedImage, actualFormat, baos)) { |
| | | compressedBytes = baos.toByteArray(); |
| | | } else { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | // 6. 构建Data URL |
| | | String base64 = Base64.getEncoder().encodeToString(compressedBytes); |
| | | if (base64 == null || base64.isEmpty()) { |
| | | log.error("Base64编码失败"); |
| | | return null; |
| | | } |
| | | |
| | | // 6. 读取图片文件并转换为Base64 |
| | | byte[] fileContent = Files.readAllBytes(tempThumbnailFile.toPath()); |
| | | String mimeType = getMimeType(actualFormat); |
| | | String dataUrl = "data:" + mimeType + ";base64," + base64; |
| | | |
| | | if (!isValidImage(fileContent)) { |
| | | return null; |
| | | } |
| | | log.info("生成Base64 Data URL成功,长度: {}, 格式: {}", |
| | | dataUrl.length(), actualFormat); |
| | | |
| | | // 7. 转换为Base64 |
| | | String base64 = Base64.getEncoder().encodeToString(fileContent); |
| | | String mimeType = getMimeType(ext); |
| | | |
| | | return "data:" + mimeType + ";base64," + base64; |
| | | return dataUrl; |
| | | |
| | | } catch (Exception e) { |
| | | log.error("生成Base64封面失败", e); |
| | | log.error("使用JavaCV生成封面失败: {}", e.getMessage(), e); |
| | | return null; |
| | | } finally { |
| | | // 清理资源 |
| | | if (process != null && process.isAlive()) { |
| | | process.destroy(); |
| | | // 7. 释放资源 |
| | | if (converter != null) { |
| | | try { |
| | | converter.close(); |
| | | } catch (Exception e) { |
| | | log.warn("关闭Java2DFrameConverter时出错", e); |
| | | } |
| | | } |
| | | |
| | | if (tempThumbnailFile != null && tempThumbnailFile.exists()) { |
| | | if (grabber != null) { |
| | | try { |
| | | tempThumbnailFile.delete(); |
| | | grabber.stop(); |
| | | grabber.release(); |
| | | } catch (Exception e) { |
| | | // ignore |
| | | log.warn("释放FFmpegFrameGrabber资源时出错", e); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | // /** |
| | | // * 处理视频文件:生成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.put("success", false); |
| | | // result.put("message", "不支持的视频格式"); |
| | | // return result; |
| | | // } |
| | | // |
| | | // // 2. 保存原始文件到临时目录 |
| | | //// String originalFilename = file.getName(); |
| | | //// tempVideoFile = File.createTempFile("video_", ".temp"); |
| | | // |
| | | // // 3. 获取视频信息 |
| | | //// MultimediaInfo info = getVideoInfo(tempVideoFile); |
| | | //// double durationSeconds = info.getDuration() / 1000.0; |
| | | //// String videoResolution = info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight(); |
| | | // |
| | | // // 4. 生成Base64格式的封面 |
| | | // String base64Thumbnail = generateBase64ThumbnailFromFile( |
| | | // tempVideoFile, |
| | | // width > 0 ? width : thumbnailWidth, |
| | | // height > 0 ? height : thumbnailHeight, |
| | | // (int) quality, |
| | | // format != null ? format : thumbnailFormat |
| | | // ); |
| | | // |
| | | // if (base64Thumbnail == null || base64Thumbnail.isEmpty()) { |
| | | // result.put("success", false); |
| | | // result.put("message", "封面生成失败"); |
| | | // return result; |
| | | // } |
| | | // |
| | | // // 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. 返回结果 |
| | | // 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.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格式的封面(不存储文件) |
| | | */ |