康鲁杰
2025-04-22 b53e164ad6a91c346931ae106de56613735241ae
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceImpl.java
@@ -92,48 +92,48 @@
    public PageResult<CourseMediaDO> getCourseMediaPage(CourseMediaPageReqVO pageReqVO) {
        PageResult<CourseMediaDO> 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 (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);
                    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);
           }
                    }
                }
                courseMediaDO.setPos(pos);
            }
        }
        return courseMediaDOPageResult;
@@ -256,215 +256,306 @@
     */
    @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);
        String mainVideoPath = "";
        // 检查是否有主视频或预览视频
        if (videoUrl != null) {
            mainVideoPath = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/"));
            videoUrls.add(mainVideoPath);
            videoUrls.add(mainVideoPath);
            videoUrl = configApi.getConfigValueByKey("easegen.url") + videoUrl.substring(videoUrl.lastIndexOf("/"));
            videoUrls.add(videoUrl);
            videoUrls.add(trailer);
        } else if (previewUrl != null) {
            mainVideoPath = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/"));
            videoUrls.add(mainVideoPath);
            videoUrls.add(mainVideoPath);
            previewUrl = configApi.getConfigValueByKey("easegen.url") + previewUrl.substring(previewUrl.lastIndexOf("/"));
            videoUrls.add(previewUrl);
            videoUrls.add(trailer);
        }
        // 提取主视频的参数
        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();
        }
        // 创建txt 文件
        try {
            createVideosFile(titles, mainVideoPath, trailer);
        // 生成视频文件列表
        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) {
            System.err.println("Error creating videos.txt file: " + e.getMessage());
            return CommonResult.error(BAD_REQUEST.getCode(), "合成失败");
            e.printStackTrace();
        }
        // 输出文件路径
        String outputFilePath = configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + ".mp4";
        // 使用 FFmpeg 合并视频并应用主视频的参数
        mergeVideos(outputFilePath, mainVideoParams,titles,mainVideoPath,trailer);
        System.out.println("Video merging completed.");
        // 去掉 courseMediaSubtitlesReqVO.getCourseName() 中的空格和特殊字符
        String newFileName = courseMediaSubtitlesReqVO.getCourseName().replaceAll("[\\s\\p{Punct}]", "");
        byte[] bytes = FileUtil.readBytes(FileUtil.file(outputFilePath));
        // 获取主视频分辨率
        String videoInfo = getVideoResolution(videoUrls.get(1)); // 使用主视频 URL
        String[] resolution = videoInfo.split("x");
        String width = resolution[0];
        String height = resolution[1];
        boolean hasAudio = checkAudio(titles);
        try {
            ProcessBuilder builder = null;
            if (hasAudio) {
                // 视频包含音频,执行相应的命令
                builder = new ProcessBuilder(
                        "ffmpeg",
                        "-i", titles,
                        "-vf", "scale="+width+":"+height+":force_original_aspect_ratio=decrease,pad="+width+":"+height+":(ow-iw)/2:(oh-ih)/2,setsar=1",
                        "-c:v", "libx264", "-preset", "veryfast",
                        "-c:a", "aac", "-shortest", configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_intro.mp4"
                );
            } else {
                // 视频不包含音频,生成空音频并合成
                builder = new ProcessBuilder(
                        "ffmpeg",
                        "-i", titles,
                        "-f", "lavfi", "-t", "10", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100",
                        "-filter_complex", "[0:v]scale="+width+":"+height+":force_original_aspect_ratio=decrease,pad="+width+":"+height+":(ow-iw)/2:(oh-ih)/2,setsar=1[v]",
                        "-map", "[v]", "-map", "1:a",
                        "-c:v", "libx264", "-preset", "veryfast",
                        "-c:a", "aac", "-shortest", configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_intro.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();
                    }
                }
            }
        } catch (Exception  e) {
            e.printStackTrace();
        }
        boolean hasAudio1 = checkAudio(trailer);
        try {
            ProcessBuilder builder = null;
            if (hasAudio1) {
                // 视频包含音频,执行相应的命令
                builder = new ProcessBuilder(
                        "ffmpeg",
                        "-i", trailer,
                        "-vf", "scale="+width+":"+height+":force_original_aspect_ratio=decrease,pad="+width+":"+height+":(ow-iw)/2:(oh-ih)/2,setsar=1",
                        "-c:v", "libx264", "-preset", "veryfast",
                        "-c:a", "aac", "-shortest", configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_outro.mp4"
                );
            } else {
                // 视频不包含音频,生成空音频并合成
                builder = new ProcessBuilder(
                        "ffmpeg",
                        "-i", trailer,
                        "-f", "lavfi", "-t", "10", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100",
                        "-filter_complex", "[0:v]scale="+width+":"+height+":force_original_aspect_ratio=decrease,pad="+width+":"+height+":(ow-iw)/2:(oh-ih)/2,setsar=1[v]",
                        "-map", "[v]", "-map", "1:a",
                        "-c:v", "libx264", "-preset", "veryfast",
                        "-c:a", "aac", "-shortest", configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_outro.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();
                    }
                }
            }
        } catch (Exception  e) {
            e.printStackTrace();
        }
        // 合并三个视频:片头、主视频、片尾
        ProcessBuilder builder10 = new ProcessBuilder(
                "ffmpeg",
                "-i", filePath + timestamp + "_intro.mp4",
                "-i", videoUrls.get(1),
                "-i", filePath + timestamp + "_outro.mp4",
                "-filter_complex", "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[v][a]",
                "-map", "[v]",
                "-map", "[a]",
                "-c:v", "libx264",
                "-preset", "veryfast",
                "-c:a", "aac",
                "-shortest",
                configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_final.mp4"
        );
        Process process10 = null;
        builder10.redirectErrorStream(true);
        try {
            process10 = builder10.start();
            // 使用 try-with-resources 确保流关闭
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process10.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            }
            // 等待 FFmpeg 进程完成
            int exitCode = process10.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("FFmpeg 合成视频失败,退出码:" + exitCode);
            }
            System.out.println("视频已成功合成");
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException("FFmpeg 合成视频异常", e);
        }
        byte[] bytes = FileUtil.readBytes(FileUtil.file(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_final.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 + "_intro.mp4");
        FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_outro.mp4");
        FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + "_final.mp4");
        FileUtil.del(configApi.getConfigValueByKey(HEYGEM_FACE2FACE) + "/compositeVideo/" + timestamp + ".txt");
        // 删除临时文件
        FileUtil.del(outputFilePath);
        System.out.println("临时文件已删除");
        if (i > 0) {
        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
    public String getVideoResolution(String videoFilePath) {
        ProcessBuilder builder = new ProcessBuilder(
                "ffprobe",
                "-v", "error", // 仅显示错误
                "-select_streams", "v:0", // 选择视频流
                "-show_entries", "stream=width,height", // 仅显示宽度和高度
                "-of", "default=noprint_wrappers=1:nokey=1", // 格式化输出
                videoFilePath // 视频文件路径
        );
        Process process = null;
        String resolution = "";
        try {
            Process process = processBuilder.start();
            process = builder.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);
            // 读取 ffprobe 输出流
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    resolution += line + " "; // 拼接宽度和高度
                }
            }
            // 等待 ffprobe 进程完成
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Parameters extracted successfully.");
            } else {
                System.err.println("Parameter extraction failed with exit code: " + exitCode);
            if (exitCode != 0) {
                throw new RuntimeException("ffprobe 执行失败,退出码:" + exitCode);
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("Error during parameter extraction: " + e.getMessage());
            throw new RuntimeException("ffprobe 执行异常", e);
        } finally {
            if (process != null) {
                try {
                    process.getInputStream().close();
                    process.getErrorStream().close();
                    process.getOutputStream().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return params;
        // 返回分辨率,格式如 "1920x1080"
        return resolution.trim().replaceAll("\\s+", "x");
    }
    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);
    private static boolean checkAudio(String inputFile) {
        try {
            Process process = processBuilder.start();
            ProcessBuilder builder = new ProcessBuilder(
                    "ffprobe", "-i", inputFile,
                    "-show_streams", "-select_streams", "a",
                    "-loglevel", "error"
            );
            Process process = builder.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
                if (line.contains("codec_type=audio")) {
                    return true; // 音频流存在
                }
            }
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Video merged successfully.");
            } else {
                System.err.println("Video merging failed with exit code: " + exitCode);
            }
            return exitCode == 0;
        } 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;
            e.printStackTrace();
            return false;
        }
    }
}