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); 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); int i = courseMediaMapper.updateById(courseMediaDO); FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".mp4"); FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".txt"); System.out.println(); if (i>0){ return CommonResult.success("合成成功"); } return CommonResult.error(BAD_REQUEST.getCode(),"合成失败"); } }