| | |
| | | */ |
| | | @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<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); |
| | | String mainVideoPath = ""; |
| | | if (videoUrl != null) { |
| | | mainVideoPath = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/")); |
| | | videoUrls.add(mainVideoPath); |
| | | videoUrls.add(mainVideoPath); |
| | | } else if (previewUrl != null) { |
| | | previewUrl = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/")); |
| | | videoUrls.add(previewUrl); |
| | | videoUrls.add(trailer); |
| | | mainVideoPath = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/")); |
| | | videoUrls.add(mainVideoPath); |
| | | videoUrls.add(mainVideoPath); |
| | | } |
| | | //判断文件夹是否存在,如果不存在就创建 |
| | | String filePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"; |
| | | // 提取主视频的参数 |
| | | List<String> 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(); |
| | | } |
| | | 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; |
| | | // 创建txt 文件 |
| | | 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(); |
| | | } |
| | | } |
| | | createVideosFile(titles, mainVideoPath, trailer); |
| | | } catch (IOException e) { |
| | | System.err.println("Error creating videos.txt file: " + e.getMessage()); |
| | | return CommonResult.error(BAD_REQUEST.getCode(), "合成失败"); |
| | | } |
| | | byte[] bytes = FileUtil.readBytes(FileUtil.file(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".mp4")); |
| | | // 输出文件路径 |
| | | 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(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".mp4"); |
| | | FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) +"/compositeVideo/"+timestamp+".txt"); |
| | | System.out.println(); |
| | | if (i>0){ |
| | | |
| | | // 删除临时文件 |
| | | FileUtil.del(outputFilePath); |
| | | System.out.println("临时文件已删除"); |
| | | |
| | | if (i > 0) { |
| | | return CommonResult.success("合成成功"); |
| | | } |
| | | return CommonResult.error(BAD_REQUEST.getCode(),"合成失败"); |
| | | 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<String> extractMainVideoParams(String mainVideoPath) { |
| | | List<String> 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<String> mainVideoParams, String introVideoPath, String mainVideoPath, String outroVideoPath) { |
| | | List<String> 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; |
| | | } |
| | | } |
| | | } |