优化 视频转换模块

This commit is contained in:
高雄
2025-12-29 16:40:00 +08:00
parent e1e146cb5f
commit b20637652a

View File

@@ -15,9 +15,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils; import org.springframework.util.CollectionUtils;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/** /**
* @author : kl * @author : kl
@@ -33,6 +38,13 @@ public class MediaFilePreviewImpl implements FilePreview {
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private static final String mp4 = "mp4"; private static final String mp4 = "mp4";
// 添加线程池管理视频转换任务
private static final ExecutorService videoConversionExecutor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 添加转换任务缓存,避免重复转换
private static final Map<String, Future<String>> conversionTasks = new HashMap<>();
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService; this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
@@ -54,6 +66,7 @@ public class MediaFilePreviewImpl implements FilePreview {
break; break;
} }
} }
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的 if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存 if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
@@ -64,12 +77,26 @@ public class MediaFilePreviewImpl implements FilePreview {
String convertedUrl = null; String convertedUrl = null;
try { try {
if (mediaTypes) { if (mediaTypes) {
convertedUrl = convertToMp4(filePath, outFilePath, fileAttribute); // 检查是否已有正在进行的转换任务
Future<String> conversionTask = conversionTasks.get(cacheName);
if (conversionTask != null && !conversionTask.isDone()) {
// 等待现有转换任务完成
convertedUrl = conversionTask.get();
} else {
// 提交新的转换任务
conversionTask = videoConversionExecutor.submit(() -> {
return convertToMp4(filePath, outFilePath, fileAttribute);
});
conversionTasks.put(cacheName, conversionTask);
convertedUrl = conversionTask.get();
}
} else { } else {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出 convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to convert media file: {}", filePath, e); logger.error("Failed to convert media file: {}", filePath, e);
// 清理失败的任务
conversionTasks.remove(cacheName);
} }
if (convertedUrl == null) { if (convertedUrl == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员"); return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
@@ -78,6 +105,8 @@ public class MediaFilePreviewImpl implements FilePreview {
// 加入缓存 // 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
} }
// 转换完成后清理任务缓存
conversionTasks.remove(cacheName);
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath)); model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
} else { } else {
model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName)); model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName));
@@ -105,8 +134,7 @@ public class MediaFilePreviewImpl implements FilePreview {
} }
private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception { private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception {
FFmpegFrameGrabber frameGrabber = FFmpegFrameGrabber.createDefault(filePath); FFmpegFrameGrabber frameGrabber = null;
Frame captured_frame;
FFmpegFrameRecorder recorder = null; FFmpegFrameRecorder recorder = null;
try { try {
File desFile = new File(outFilePath); File desFile = new File(outFilePath);
@@ -114,6 +142,7 @@ public class MediaFilePreviewImpl implements FilePreview {
if (desFile.exists()) { if (desFile.exists()) {
return outFilePath; return outFilePath;
} }
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录 if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容 int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outFilePath.substring(0, index); String folder = outFilePath.substring(0, index);
@@ -123,45 +152,144 @@ public class MediaFilePreviewImpl implements FilePreview {
path.mkdirs(); path.mkdirs();
} }
} }
frameGrabber = FFmpegFrameGrabber.createDefault(filePath);
frameGrabber.start(); frameGrabber.start();
recorder = new FFmpegFrameRecorder(outFilePath, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
// recorder.setImageHeight(640); // 优化:使用更快的编码预设
// recorder.setImageWidth(480); recorder = new FFmpegFrameRecorder(outFilePath,
frameGrabber.getImageWidth(),
frameGrabber.getImageHeight(),
frameGrabber.getAudioChannels());
// 设置快速编码参数
recorder.setFormat(mp4); recorder.setFormat(mp4);
recorder.setFrameRate(frameGrabber.getFrameRate()); recorder.setFrameRate(frameGrabber.getFrameRate());
recorder.setSampleRate(frameGrabber.getSampleRate()); recorder.setSampleRate(frameGrabber.getSampleRate());
//视频编码属性配置 H.264 H.265 MPEG
// 视频编码属性配置 - 使用快速编码预设
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频比特率,单位:b recorder.setVideoOption("preset", "veryfast"); // 添加快速编码预设
recorder.setVideoOption("tune", "zerolatency"); // 降低延迟
recorder.setVideoBitrate(frameGrabber.getVideoBitrate()); recorder.setVideoBitrate(frameGrabber.getVideoBitrate());
recorder.setAspectRatio(frameGrabber.getAspectRatio()); recorder.setAspectRatio(frameGrabber.getAspectRatio());
// 设置音频通用编码格式
// 音频编码设置
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
//设置音频比特率,单位:b (比特率越高,清晰度/音质越好,当然文件也就越大 128000 = 182kb)
recorder.setAudioBitrate(frameGrabber.getAudioBitrate()); recorder.setAudioBitrate(frameGrabber.getAudioBitrate());
recorder.setAudioOptions(frameGrabber.getAudioOptions());
recorder.setAudioChannels(frameGrabber.getAudioChannels()); recorder.setAudioChannels(frameGrabber.getAudioChannels());
recorder.start();
while (true) { // 优化当源文件已经是h264/aac编码时尝试直接复制流
captured_frame = frameGrabber.grabFrame(); if (isCompatibleCodec(frameGrabber)) {
if (captured_frame == null) { recorder.setVideoOption("c:v", "copy");
System.out.println("转码完成:" + filePath); recorder.setAudioOption("c:a", "copy");
break;
}
recorder.record(captured_frame);
}
} catch (Exception e) {
logger.error("Failed to convert video file to mp4: {}", filePath, e);
return null;
} finally {
if (recorder != null) { //关闭
recorder.stop();
recorder.close();
}
frameGrabber.stop();
frameGrabber.close();
}
return outFilePath;
} }
recorder.start();
// 批量处理帧,提高处理效率
Frame capturedFrame;
int batchSize = 100; // 批量处理的帧数
int frameCount = 0;
Frame[] frameBatch = new Frame[batchSize];
while (true) {
capturedFrame = frameGrabber.grabFrame();
if (capturedFrame == null) {
break;
}
frameBatch[frameCount % batchSize] = capturedFrame;
frameCount++;
// 批量记录帧
if (frameCount % batchSize == 0 || capturedFrame == null) {
for (int i = 0; i < Math.min(batchSize, frameCount); i++) {
if (frameBatch[i] != null) {
recorder.record(frameBatch[i]);
frameBatch[i] = null; // 释放引用
}
}
}
}
// 记录剩余的帧
for (int i = 0; i < frameBatch.length; i++) {
if (frameBatch[i] != null) {
recorder.record(frameBatch[i]);
}
}
logger.info("视频转码完成: {} -> {}", filePath, outFilePath);
return outFilePath;
} catch (Exception e) {
logger.error("Failed to convert video file to mp4: {}", filePath, e);
// 删除可能已创建的失败文件
try {
File failedFile = new File(outFilePath);
if (failedFile.exists()) {
failedFile.delete();
}
} catch (SecurityException ex) {
logger.warn("无法删除失败的转换文件: {}", outFilePath, ex);
}
throw e;
} finally {
// 确保资源被正确释放
if (recorder != null) {
try {
recorder.stop();
recorder.close();
} catch (Exception e) {
logger.warn("关闭recorder时发生异常", e);
}
}
if (frameGrabber != null) {
try {
frameGrabber.stop();
frameGrabber.close();
} catch (Exception e) {
logger.warn("关闭frameGrabber时发生异常", e);
}
}
// 强制垃圾回收释放FFmpeg相关资源
System.gc();
}
}
/**
* 检查源文件是否已经是兼容的编码格式H264/AAC
*/
private static boolean isCompatibleCodec(FFmpegFrameGrabber grabber) {
try {
String videoCodec = grabber.getVideoCodecName();
String audioCodec = grabber.getAudioCodecName();
boolean videoCompatible = videoCodec != null &&
(videoCodec.toLowerCase().contains("h264") ||
videoCodec.toLowerCase().contains("h.264"));
boolean audioCompatible = audioCodec != null &&
(audioCodec.toLowerCase().contains("aac") ||
audioCodec.toLowerCase().contains("mp3"));
return videoCompatible && audioCompatible;
} catch (Exception e) {
logger.debug("无法获取编解码器信息", e);
return false;
}
}
/**
* 清理所有转换任务(应用关闭时调用)
*/
public static void shutdown() {
if (!CollectionUtils.isEmpty(conversionTasks)) {
conversionTasks.values().forEach(task -> task.cancel(true));
conversionTasks.clear();
}
videoConversionExecutor.shutdownNow();
}
} }