shenrongliang
2025-04-21 0aa78d876fe4819fb9b7ffc24b9a01e2c9869414
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceImpl.java
@@ -256,94 +256,215 @@
     */
    @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;
        }
    }
}
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceUtil.java
@@ -167,7 +167,7 @@
                        newFileName2 // 输出文件名
                );
            } else if ("2".equals(scene.getHasPerson())) {
                // 当没有人像时,substring1放在 cover1 的下层
                //cover1放在substring1的,让cover1挡住substring1,背景图ppt内容和人像视频不变,只是层级关系修改一下
                builder = new ProcessBuilder(
                        "ffmpeg",
                        "-i", cover1, // 背景图
@@ -175,11 +175,11 @@
                        "-i", substring1, // 人像视频
                        "-filter_complex",
                        "[0:v]scale=" + Math.round(scene.getBackground().getWidth()) + ":" + Math.round(scene.getBackground().getHeight()) + "[bg];" +
                                "[1:v]scale=" + Math.round(scene.getComponents().get(1).getWidth()) + ":" + Math.round(scene.getComponents().get(1).getHeight()) + "[v1];" +
                                "[bg][v1]overlay=x=" + Math.round(scene.getComponents().get(1).getMarginLeft()) + ":y=" + Math.round(scene.getComponents().get(1).getTop()) + "[img];" +
                                "[2:v]scale=" + Math.round(scene.getComponents().get(0).getWidth()) + ":" + Math.round(scene.getComponents().get(0).getHeight()) + "[v2];" +
                                "[img][v2]overlay=x=" + Math.round(scene.getComponents().get(0).getMarginLeft()) + ":y=" + Math.round(scene.getComponents().get(0).getTop()),
                        newFileName2 // 输出文件名
                        "[1:v]scale=" + Math.round(scene.getComponents().get(1).getWidth()) + ":" + Math.round(scene.getComponents().get(1).getHeight()) + "[v1];" +
                        "[bg][v1]overlay=x=" + Math.round(scene.getComponents().get(1).getMarginLeft()) + ":y=" + Math.round(scene.getComponents().get(1).getTop()) + "[img];" +
                        "[2:v]scale=" + Math.round(scene.getComponents().get(0).getWidth()) + ":" + Math.round(scene.getComponents().get(0).getHeight()) + "[v2];" +
                        "[v2][img]overlay=x=" + Math.round(scene.getComponents().get(0).getMarginLeft()) + ":y=" + Math.round(scene.getComponents().get(0).getTop()),
                        newFileName2
                );
            }
            System.out.println(newFileName2);