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;
|
import org.springframework.stereotype.Service;
|
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.util.*;
|
import java.util.Base64;
|
import java.util.List;
|
import java.util.concurrent.TimeUnit;
|
|
@Service
|
@Slf4j
|
public class VideoProcessService {
|
|
@Value("${video.upload-dir:./uploads/videos}")
|
private String uploadDir;
|
|
@Value("${video.thumbnail.width:320}")
|
private int thumbnailWidth;
|
|
@Value("${video.thumbnail.height:240}")
|
private int thumbnailHeight;
|
|
@Value("${video.thumbnail.quality:5}")
|
private int thumbnailQuality; // 1-31,值越小质量越好
|
|
@Value("${video.thumbnail.format:jpg}")
|
private String thumbnailFormat;
|
|
// 支持的视频格式
|
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 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.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格式的封面(不存储文件)
|
*/
|
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());
|
}
|
}
|
|
/**
|
* 检查是否为支持的视频文件
|
*/
|
private boolean isVideoFile(MultipartFile file) {
|
if (file == null || file.isEmpty()) {
|
return false;
|
}
|
|
String fileName = file.getOriginalFilename();
|
if (fileName == null) {
|
return false;
|
}
|
|
String extension = getFileExtension(fileName).toLowerCase();
|
return SUPPORTED_VIDEO_FORMATS.contains(extension);
|
}
|
|
/**
|
* 检查是否为支持的视频文件
|
*/
|
private boolean isVideoFile(File file) {
|
if (file == null || !file.exists()) {
|
return false;
|
}
|
|
String fileName = file.getName();
|
String extension = getFileExtension(fileName).toLowerCase();
|
return SUPPORTED_VIDEO_FORMATS.contains(extension);
|
}
|
|
/**
|
* 批量生成Base64格式的缩略图
|
*/
|
// 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;
|
// }
|
|
/**
|
* 获取视频时长
|
*/
|
public long getVideoDuration(File videoFile) throws Exception {
|
MultimediaInfo info = getVideoInfo(videoFile);
|
return info.getDuration() / 1000; // 返回秒数
|
}
|
|
/**
|
* 获取视频分辨率
|
*/
|
public String getVideoResolution(File videoFile) throws Exception {
|
MultimediaInfo info = getVideoInfo(videoFile);
|
VideoSize size = info.getVideo().getSize();
|
return size.getWidth() + "x" + size.getHeight();
|
}
|
|
/**
|
* 获取视频详细信息
|
*/
|
public VideoInfo getVideoInfoDetails(File videoFile) throws Exception {
|
VideoInfo info = new VideoInfo();
|
MultimediaInfo multimediaInfo = getVideoInfo(videoFile);
|
|
info.setFileName(videoFile.getName());
|
info.setFileSize(videoFile.length());
|
info.setDuration(multimediaInfo.getDuration() / 1000.0);
|
info.setResolution(multimediaInfo.getVideo().getSize().getWidth() + "x" +
|
multimediaInfo.getVideo().getSize().getHeight());
|
info.setVideoFormat(multimediaInfo.getFormat());
|
info.setBitRate(multimediaInfo.getVideo().getBitRate());
|
info.setFrameRate(multimediaInfo.getVideo().getFrameRate());
|
|
return info;
|
}
|
|
/**
|
* 获取文件扩展名
|
*/
|
private String getFileExtension(String fileName) {
|
if (fileName == null) return "";
|
int lastDot = fileName.lastIndexOf('.');
|
if (lastDot > 0 && lastDot < fileName.length() - 1) {
|
return fileName.substring(lastDot + 1).toLowerCase();
|
}
|
return "";
|
}
|
|
/**
|
* 获取不带扩展名的文件名
|
*/
|
private String getFileNameWithoutExtension(String fileName) {
|
if (fileName == null) return "";
|
int lastDot = fileName.lastIndexOf('.');
|
if (lastDot > 0) {
|
return fileName.substring(0, lastDot);
|
}
|
return fileName;
|
}
|
|
/**
|
* 验证图片是否有效
|
*/
|
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 "image/jpeg";
|
case "png":
|
return "image/png";
|
case "gif":
|
return "image/gif";
|
case "bmp":
|
return "image/bmp";
|
case "webp":
|
return "image/webp";
|
default:
|
return "image/jpeg";
|
}
|
}
|
|
/**
|
* 判断操作系统是否是Windows
|
*/
|
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 originalFilename;
|
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);
|
}
|
|
public String getDurationFormatted() {
|
return formatDuration(duration);
|
}
|
}
|
|
@Data
|
public static class BatchThumbnailResult {
|
private List<VideoProcessResult> results;
|
private int total;
|
private int success;
|
private int fail;
|
|
public String getSuccessRate() {
|
if (total == 0) return "0%";
|
return String.format("%.1f%%", (success * 100.0) / total);
|
}
|
}
|
|
@Data
|
public static class VideoInfo {
|
private String fileName;
|
private long fileSize;
|
private double duration; // 秒
|
private String resolution;
|
private String videoFormat;
|
private int bitRate; // 比特率
|
private float frameRate; // 帧率
|
|
public String getFileSizeFormatted() {
|
return formatFileSize(fileSize);
|
}
|
|
public String getDurationFormatted() {
|
return formatDuration(duration);
|
}
|
}
|
|
/**
|
* 格式化文件大小
|
*/
|
public static String formatFileSize(long size) {
|
if (size < 1024) {
|
return size + " B";
|
} else if (size < 1024 * 1024) {
|
return String.format("%.1f KB", size / 1024.0);
|
} else if (size < 1024 * 1024 * 1024) {
|
return String.format("%.1f MB", size / (1024.0 * 1024.0));
|
} else {
|
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
|
}
|
}
|
|
/**
|
* 格式化时长
|
*/
|
public static String formatDuration(double seconds) {
|
if (seconds < 60) {
|
return String.format("%.1f秒", seconds);
|
} else if (seconds < 3600) {
|
int minutes = (int) (seconds / 60);
|
double remainingSeconds = seconds % 60;
|
return String.format("%d分%.1f秒", minutes, remainingSeconds);
|
} else {
|
int hours = (int) (seconds / 3600);
|
int minutes = (int) ((seconds % 3600) / 60);
|
double remainingSeconds = seconds % 60;
|
return String.format("%d小时%d分%.1f秒", hours, minutes, remainingSeconds);
|
}
|
}
|
}
|