package cn.iocoder.yudao.module.digitalcourse.service.coursemedia; import cn.hutool.core.io.FileUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursemedia.vo.*; import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.coursemedia.CourseMediaDO; import cn.iocoder.yudao.module.digitalcourse.dal.mysql.coursemedia.CourseMediaMapper; import cn.iocoder.yudao.module.digitalcourse.manager.MediaTaskManager; import cn.iocoder.yudao.module.digitalcourse.model.MediaTask; import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.fastjson.JSON; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import java.io.*; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Date; import java.util.List; import static cn.iocoder.yudao.module.digitalcourse.enums.ErrorCodeConstants.COURSE_MEDIA_NOT_EXISTS; /** * 课程媒体 Service 实现类 * * @author 芋道源码 */ @Service @Validated @Slf4j public class CourseMediaServiceImpl implements CourseMediaService { @Resource private CourseMediaMapper courseMediaMapper; @Resource private CourseMediaServiceUtil courseMediaServiceUtil; @Resource private FileApi fileApi; @Override public Long createCourseMedia(CourseMediaSaveReqVO createReqVO) { // 插入 CourseMediaDO courseMedia = BeanUtils.toBean(createReqVO, CourseMediaDO.class); courseMediaMapper.insert(courseMedia); // 返回 return courseMedia.getId(); } @Override public void updateCourseMedia(CourseMediaSaveReqVO updateReqVO) { // 校验存在 validateCourseMediaExists(updateReqVO.getId()); // 更新 CourseMediaDO updateObj = BeanUtils.toBean(updateReqVO, CourseMediaDO.class); courseMediaMapper.updateById(updateObj); } @Override public void deleteCourseMedia(Long id) { // 校验存在 validateCourseMediaExists(id); // 删除 courseMediaMapper.deleteById(id); } private void validateCourseMediaExists(Long id) { if (courseMediaMapper.selectById(id) == null) { throw exception(COURSE_MEDIA_NOT_EXISTS); } } @Override public CourseMediaDO getCourseMedia(Long id) { return courseMediaMapper.selectById(id); } @Override public PageResult getCourseMediaPage(CourseMediaPageReqVO pageReqVO) { PageResult courseMediaDOPageResult = courseMediaMapper.selectPage(pageReqVO); for (CourseMediaDO courseMediaDO : courseMediaDOPageResult.getList()) { if (courseMediaDO.getStatus() == 1 || courseMediaDO.getStatus() == 0) { //视频合成中 查询排队和合成进度 Long id = courseMediaDO.getCourseId(); int pos = mediaTaskManager.getQueuePosition(id); if (pos == -1) { //不在队列中,说明已经合成完成 courseMediaDO.setStatus(3); courseMediaMapper.updateById(courseMediaDO); } if (pos == 0) { //正在合成中 String reqJson = courseMediaDO.getReqJson(); CourseMediaMegerVO courseMediaMegerVO = JSON.parseObject(reqJson, CourseMediaMegerVO.class); int size = courseMediaMegerVO.getScenes().size(); String s = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/temp/"; //查询s下面的文件 File folder = new File(s); int count = 0; if (folder.exists() && folder.isDirectory()) { File[] files = folder.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().endsWith("-r.mp4")) { count++; System.out.println("匹配文件: " + file.getName()); } } } System.out.println("总计匹配 -r.mp4 文件数量: " + count); } else { System.out.println("路径不存在或不是目录"); } if (count+1>size){ courseMediaDO.setProgressVideo((count) + "/" + size); }else{ courseMediaDO.setProgressVideo((count)+"/"+size); } } courseMediaDO.setPos(pos); } } return courseMediaDOPageResult; } private static final String HEYGEM_FACE2FACE = "heygem.face2face"; @Resource private ConfigApi configApi; @Override public CommonResult megerMedia(CourseMediaMegerVO updateReqVO) { Long id = updateReqVO.getId(); CourseMediaDO courseMediaDO = courseMediaMapper.selectOne(new QueryWrapperX().lambda().eq(CourseMediaDO::getCourseId,id).in(CourseMediaDO::getStatus,0,1)); if (courseMediaDO == null){ courseMediaDO = new CourseMediaDO(); courseMediaDO.setCourseId(id); courseMediaDO.setStatus(0); courseMediaDO.setMediaType(1); courseMediaDO.setName(updateReqVO.getName()); courseMediaDO.setCourseName(updateReqVO.getName()); // courseMediaDO.setAuditVo(updateReqVO.getAuditionVo()); //将updateReqVO 转换为json字符串 courseMediaDO.setReqJson(JSON.toJSONString(updateReqVO)); courseMediaMapper.insert(courseMediaDO); }else{ return CommonResult.error(BAD_REQUEST.getCode(),"已存在合成中视频,不允许重复合成"); } updateReqVO.setCourseMediaId(courseMediaDO.getId()); //异步调用数字人视频渲染接口,开始合并 MediaTask task = new MediaTask(id, updateReqVO, LocalDateTime.now()); int pos = 0; try { pos = mediaTaskManager.submitTask(task); } catch (IOException e) { throw new RuntimeException(e); } return CommonResult.success("合成视频提交成功,您排在第 " + (pos+1) + " 个"); } @Resource private MediaTaskManager mediaTaskManager; @Override public CommonResult reMegerMedia(CourseMediaMegerVO updateReqVO) { Long id = updateReqVO.getId(); CourseMediaDO courseMediaDO = courseMediaMapper.selectById(updateReqVO.getId()); if (courseMediaDO == null){ return CommonResult.error(BAD_REQUEST.getCode(),"未查询到合成视频记录"); } if (3!=courseMediaDO.getStatus()){ return CommonResult.error(BAD_REQUEST.getCode(),"只有失败状态视频允许重新合成视频"); } //异步调用数字人视频渲染接口,开始合并 Boolean success = courseMediaServiceUtil.reMegerMedia(courseMediaDO); if(success) { return CommonResult.success(true); } else { return CommonResult.error(BAD_REQUEST.getCode(),"视频重新合成失败"); } } @Override public CommonResult createSubtitles(CourseMediaSubtitlesReqVO courseMediaSubtitlesReqVO) { CourseMediaDO courseMediaDO1 = courseMediaMapper.selectOne(new QueryWrapperX().lambda().eq(CourseMediaDO::getId, courseMediaSubtitlesReqVO.getId())); if (courseMediaDO1.getSubtitlesStatus()!= null && courseMediaDO1.getSubtitlesStatus() == 1) { throw new RuntimeException("字幕生成中,请勿重复提交"); } CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setSubtitlesStatus(1); courseMediaMapper.updateById(courseMediaDO); courseMediaServiceUtil.createSubtitles(courseMediaSubtitlesReqVO); return CommonResult.success("视频字幕生成中,请稍后查看"); } @Override public CommonResult createSubtitlesVideo(CourseMediaSubtitlesReqVO courseMediaSubtitlesReqVO) { CourseMediaDO courseMediaDO1 = courseMediaMapper.selectOne(new QueryWrapperX().lambda().eq(CourseMediaDO::getId, courseMediaSubtitlesReqVO.getId()).eq(CourseMediaDO::getSubtitlesStatus, 2)); if (courseMediaDO1 == null) { return CommonResult.error(BAD_REQUEST.getCode(), "字幕文件不存在或未生成"); } CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setSubtitlesAddStatus(1); courseMediaMapper.updateById(courseMediaDO); courseMediaServiceUtil.createSubtitlesVideo(courseMediaDO1); return CommonResult.success("视频添加字幕中,请稍后查看"); } @Override public void updateSubtitles(CourseMediaEditSReqVO updateReqVO) { // 校验存在 validateCourseMediaExists(updateReqVO.getId()); // 更新 CourseMediaDO updateObj = BeanUtils.toBean(updateReqVO, CourseMediaDO.class); updateObj.setSubtitlesStatus(2); courseMediaMapper.updateById(updateObj); } /** * 上传片头片尾 * @param courseMediaSubtitlesReqVO * @return */ @Override public CommonResult createTrailer(CourseMediaSubtitlesReqVO courseMediaSubtitlesReqVO) { CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setTrailer(courseMediaSubtitlesReqVO.getTrailer()); courseMediaDO.setTitles(courseMediaSubtitlesReqVO.getTitles()); int i = courseMediaMapper.updateById(courseMediaDO); if (i>0){ return CommonResult.success("片头片尾上传成功"); } return CommonResult.error(BAD_REQUEST.getCode(),"片头片尾上传失败"); } /** * 合成片头片尾视频 * @param courseMediaSubtitlesReqVO * @return */ @Override public CommonResult 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 videoUrls = new ArrayList<>(); videoUrls.add(titles); String mainVideoPath = ""; if (videoUrl != null) { mainVideoPath = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/")); videoUrls.add(mainVideoPath); videoUrls.add(mainVideoPath); } else if (previewUrl != null) { mainVideoPath = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/")); videoUrls.add(mainVideoPath); videoUrls.add(mainVideoPath); } // 提取主视频的参数 List mainVideoParams = extractMainVideoParams(mainVideoPath); if (mainVideoParams.isEmpty()) { System.err.println("Failed to extract parameters from main video."); return CommonResult.error(BAD_REQUEST.getCode(), "合成失败"); } // 判断文件夹是否存在,如果不存在就创建 String filePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/"; File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } // 创建txt 文件 try { createVideosFile(titles, mainVideoPath, trailer); } catch (IOException e) { System.err.println("Error creating videos.txt file: " + e.getMessage()); return CommonResult.error(BAD_REQUEST.getCode(), "合成失败"); } // 输出文件路径 String outputFilePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + ".mp4"; // 使用 FFmpeg 合并视频并应用主视频的参数 mergeVideos(outputFilePath, mainVideoParams,titles,mainVideoPath,trailer); System.out.println("Video merging completed."); byte[] bytes = FileUtil.readBytes(FileUtil.file(outputFilePath)); String compositeVideo = fileApi.createFile(bytes); // 更新数据库记录 CourseMediaDO courseMediaDO = new CourseMediaDO(); courseMediaDO.setId(courseMediaSubtitlesReqVO.getId()); courseMediaDO.setCompositeVideo(compositeVideo); int i = courseMediaMapper.updateById(courseMediaDO); // 删除临时文件 FileUtil.del(outputFilePath); System.out.println("临时文件已删除"); if (i > 0) { return CommonResult.success("合成成功"); } return CommonResult.error(BAD_REQUEST.getCode(), "合成失败"); } private static void createVideosFile(String introVideoPath, String mainVideoPath, String outroVideoPath) throws IOException { File videosFile = new File("videos.txt"); StringBuilder content = new StringBuilder(); content.append("file '").append(introVideoPath).append("'\n"); content.append("file '").append(mainVideoPath).append("'\n"); content.append("file '").append(outroVideoPath).append("'\n"); java.nio.file.Files.write(videosFile.toPath(), content.toString().getBytes()); } private static List extractMainVideoParams(String mainVideoPath) { List params = new ArrayList<>(); ProcessBuilder processBuilder = new ProcessBuilder( "ffmpeg", "-i", mainVideoPath ); try { Process process = processBuilder.start(); BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); String line; while ((line = errorReader.readLine()) != null) { if (line.contains("Video:")) { String videoInfo = line.split(":")[1].trim(); String codec = videoInfo.split(", ")[0]; params.add("-c:v"); params.add(codec); } else if (line.contains("Audio:")) { String audioInfo = line.split(":")[1].trim(); String codec = audioInfo.split(", ")[0]; String bitrate = audioInfo.split(", ")[1].split("\\s+")[0]; params.add("-c:a"); params.add(codec); params.add("-b:a"); params.add(bitrate); } } int exitCode = process.waitFor(); if (exitCode == 0) { System.out.println("Parameters extracted successfully."); } else { System.err.println("Parameter extraction failed with exit code: " + exitCode); } } catch (IOException | InterruptedException e) { System.err.println("Error during parameter extraction: " + e.getMessage()); } return params; } private static void mergeVideos(String outputPath, List mainVideoParams, String introVideoPath, String mainVideoPath, String outroVideoPath) { List command = new ArrayList<>(); command.add("ffmpeg"); command.add("-f"); command.add("concat"); command.add("-safe"); command.add("0"); command.add("-i"); command.add("videos.txt"); // 添加映射选项以确保音频流的一致性 command.add("-map"); command.add("[v]"); command.add("-map"); command.add("[a]"); // 添加过滤器选项以处理无声音情况 double introDuration = getDurationInSeconds(introVideoPath); double mainDuration = getDurationInSeconds(mainVideoPath); double outroDuration = getDurationInSeconds(outroVideoPath); StringBuilder filterComplex = new StringBuilder(); filterComplex.append("[0:v][0:a?]overlay=enable='between(t,0,"); filterComplex.append(introDuration); filterComplex.append(")'[v0];"); filterComplex.append("[1:v][1:a?]overlay=enable='between(t,"); filterComplex.append(introDuration); filterComplex.append(","); filterComplex.append(introDuration + mainDuration); filterComplex.append(")'[v1];"); filterComplex.append("[2:v][2:a?]overlay=enable='gt(t,"); filterComplex.append(introDuration + mainDuration); filterComplex.append(")'[v2];"); filterComplex.append("[v0][v1][v2]concat=n=3:v=1:a=0[v];"); filterComplex.append("[0:a]aresample=async=1:first_pts=0[a0];"); filterComplex.append("[1:a]aresample=async=1:first_pts=0[a1];"); filterComplex.append("[2:a]aresample=async=1:first_pts=0[a2];"); filterComplex.append("[a0][a1][a2]amerge=inputs=3[a]"); command.add("-filter_complex"); command.add(filterComplex.toString()); // 添加主视频的参数 command.addAll(mainVideoParams); command.add(outputPath); ProcessBuilder processBuilder = new ProcessBuilder(command); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } int exitCode = process.waitFor(); if (exitCode == 0) { System.out.println("Video merged successfully."); } else { System.err.println("Video merging failed with exit code: " + exitCode); } } catch (IOException | InterruptedException e) { System.err.println("Error during video merging: " + e.getMessage()); } } private static double getDurationInSeconds(String videoPath) { ProcessBuilder processBuilder = new ProcessBuilder( "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath ); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String durationStr = reader.readLine(); double duration = Double.parseDouble(durationStr); process.waitFor(); return duration; } catch (IOException | InterruptedException | NumberFormatException e) { System.err.println("Error getting duration of video: " + e.getMessage()); return 0.0; } } }