yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceImpl.java
@@ -212,7 +212,7 @@ return CommonResult.error(BAD_REQUEST.getCode(), "字幕文件不存在或未生成"); } CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setCourseId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setSubtitlesAddStatus(1); courseMediaMapper.updateById(courseMediaDO); courseMediaServiceUtil.createSubtitlesVideo(courseMediaDO1); @@ -254,71 +254,8 @@ */ @Override public CommonResult createCompositeVideo(CourseMediaSubtitlesReqVO courseMediaSubtitlesReqVO) { //片头地址 String titles = courseMediaSubtitlesReqVO.getTitles(); titles = configApi.getConfigValueByKey("easegen.url") + titles.substring(titles.lastIndexOf("/")); //片尾地址 String trailer = courseMediaSubtitlesReqVO.getTrailer(); trailer = configApi.getConfigValueByKey("easegen.url") + trailer.substring(trailer.lastIndexOf("/")); String videoUrl = courseMediaSubtitlesReqVO.getVideoUrl(); String previewUrl = courseMediaSubtitlesReqVO.getPreviewUrl(); List<String> videoUrls = new ArrayList<>(); videoUrls.add(titles); if (videoUrl != null){ videoUrl = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/")); videoUrls.add(videoUrl); videoUrls.add(trailer); } else if (previewUrl != null) { previewUrl = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/")); videoUrls.add(previewUrl); videoUrls.add(trailer); } //判断文件夹是否存在,如果不存在就创建 String filePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"; File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } String fileListPath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/filelist.txt"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileListPath))) { for (String path : videoUrls) { writer.write("file '" + path + "'\n"); } System.out.println("文件列表已生成:" + fileListPath); } catch (IOException e) { e.printStackTrace(); } //去掉updateReqVO.getName()中的空格和特殊字符 String newFileName = courseMediaSubtitlesReqVO.getCourseName().replaceAll("[\\s\\p{Punct}]", ""); ProcessBuilder builder = new ProcessBuilder( "ffmpeg", "-f", "concat", "-safe","0", "-i",fileListPath , "-c", "copy", configApi.getConfigValueByKey(HEYGEM_FACE2FACE)+"/compositeVideo/"+newFileName+".mp4" // q ); builder.redirectErrorStream(true); Process process = null; try { process = builder.start(); // 读取 FFmpeg 输出(可选) BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); System.out.println(builder.command()); System.out.println("最终视频已生成"); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { throw new RuntimeException(e); } byte[] bytes = FileUtil.readBytes(FileUtil.file(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+newFileName+".mp4")); String compositeVideo = fileApi.createFile(bytes); CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setCompositeVideo(compositeVideo); int i = courseMediaMapper.updateById(courseMediaDO); if (i>0){ return CommonResult.success("视频合成成功"); } return CommonResult.error(BAD_REQUEST.getCode(),"视频合成失败"); //异步合成 courseMediaServiceUtil.createCompositeVideo(courseMediaSubtitlesReqVO); return CommonResult.success("视频合成中,请稍后查看"); } } yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceUtil.java
@@ -6,6 +6,7 @@ import cn.hutool.http.HttpException; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursemedia.vo.CourseMediaMegerVO; import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursemedia.vo.CourseMediaSubtitlesReqVO; @@ -42,8 +43,11 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; @Service @Validated @@ -163,7 +167,7 @@ newFileName2 // 输出文件名 ); } else if ("2".equals(scene.getHasPerson())) { // 当没有人像时,视频放在 cover 的下层 // 当没有人像时,substring1放在 cover1 的下层 builder = new ProcessBuilder( "ffmpeg", "-i", cover1, // 背景图 @@ -691,4 +695,93 @@ throw new RuntimeException(e); } } @Async public void createCompositeVideo(CourseMediaSubtitlesReqVO courseMediaSubtitlesReqVO) { // 生成时间戳 String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); //片头地址 String titles = courseMediaSubtitlesReqVO.getTitles(); titles = configApi.getConfigValueByKey("easegen.url") + titles.substring(titles.lastIndexOf("/")); //片尾地址 String trailer = courseMediaSubtitlesReqVO.getTrailer(); trailer = configApi.getConfigValueByKey("easegen.url") + trailer.substring(trailer.lastIndexOf("/")); String videoUrl = courseMediaSubtitlesReqVO.getVideoUrl(); String previewUrl = courseMediaSubtitlesReqVO.getPreviewUrl(); List<String> videoUrls = new ArrayList<>(); videoUrls.add(titles); if (videoUrl != null){ videoUrl = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/")); videoUrls.add(videoUrl); videoUrls.add(trailer); } else if (previewUrl != null) { previewUrl = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/")); videoUrls.add(previewUrl); videoUrls.add(trailer); } //判断文件夹是否存在,如果不存在就创建 String filePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"; File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } String fileListPath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".txt"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileListPath))) { for (String path : videoUrls) { writer.write("file '" + path + "'\n"); } System.out.println("文件列表已生成:" + fileListPath); } catch (IOException e) { e.printStackTrace(); } //去掉updateReqVO.getName()中的空格和特殊字符 String newFileName = courseMediaSubtitlesReqVO.getCourseName().replaceAll("[\\s\\p{Punct}]", ""); ProcessBuilder builder = new ProcessBuilder( "ffmpeg", "-f", "concat", "-safe", "0", "-i", fileListPath, "-c", "copy", configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + ".mp4" ); builder.redirectErrorStream(true); Process process = null; try { process = builder.start(); // 使用 try-with-resources 确保流关闭 try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } // 等待 FFmpeg 进程完成 int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("FFmpeg 执行失败,退出码:" + exitCode); } System.out.println("最终视频已生成"); } catch (IOException | InterruptedException e) { throw new RuntimeException("FFmpeg 执行异常", e); } finally { // 确保 Process 的输入/错误流被关闭 if (process != null) { try { process.getInputStream().close(); process.getErrorStream().close(); process.getOutputStream().close(); } catch (IOException e) { e.printStackTrace(); } } } byte[] bytes = FileUtil.readBytes(FileUtil.file(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".mp4")); String compositeVideo = fileApi.createFile(bytes); CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setCompositeVideo(compositeVideo); courseMediaMapper.updateById(courseMediaDO); FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".mp4"); FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".txt"); System.out.println(); } } yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
@@ -8,6 +8,7 @@ import org.apache.tika.Tika; import java.io.IOException; import java.io.OutputStream; import java.net.URLEncoder; /** @@ -65,12 +66,33 @@ response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { response.setHeader("Content-Length", String.valueOf(content.length - 1)); response.setHeader("Content-Range", String.valueOf(content.length - 1)); long contentLength = content.length; response.setHeader("Content-Length", String.valueOf(contentLength)); response.setHeader("Content-Range", "bytes 0-" + (contentLength - 1) + "/" + contentLength); response.setHeader("Accept-Ranges", "bytes"); } // 输出附件 IoUtil.write(response.getOutputStream(), false, content); OutputStream outputStream = null; try { outputStream = response.getOutputStream(); IoUtil.write(outputStream, false, content); } catch (IOException e) { if ("Connection reset by peer".equals(e.getMessage())) { System.out.println("客户端中断连接: {}"); System.out.println(e.getMessage()); } else { throw e; } } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { System.out.println("关闭输出流失败: {}"); System.out.println(e.getMessage()); } } } } }