Parcourir la source

update oss-store

reghao il y a 1 mois
Parent
commit
e051af8d23

+ 17 - 58
oss-store/src/main/java/cn/reghao/oss/store/media/VideoFileProcessor.java

@@ -19,8 +19,9 @@ import java.util.regex.Pattern;
  */
  */
 @Slf4j
 @Slf4j
 public class VideoFileProcessor {
 public class VideoFileProcessor {
-    static final ObjectMapper mapper = new ObjectMapper();
-
+    private final static String ffprobe = "/usr/bin/ffprobe";
+    private final static String ffmpeg = "/usr/bin/ffmpeg";
+    private static final ObjectMapper mapper = new ObjectMapper();
 
 
     /**
     /**
      * 获取视频最详尽的信息:格式、所有流、程序、章节、元数据
      * 获取视频最详尽的信息:格式、所有流、程序、章节、元数据
@@ -28,7 +29,7 @@ public class VideoFileProcessor {
     public static JsonNode getVideoMetadata(String filePath) {
     public static JsonNode getVideoMetadata(String filePath) {
         // 构建全量探测命令
         // 构建全量探测命令
         List<String> cmd = Arrays.asList(
         List<String> cmd = Arrays.asList(
-                "ffprobe",
+                ffprobe,
                 "-v", "quiet",           // 不打印日志头
                 "-v", "quiet",           // 不打印日志头
                 "-print_format", "json", // 输出 JSON
                 "-print_format", "json", // 输出 JSON
                 "-show_format",          // 容器格式信息 (bitrate, size, duration, tags)
                 "-show_format",          // 容器格式信息 (bitrate, size, duration, tags)
@@ -77,16 +78,18 @@ public class VideoFileProcessor {
 
 
     public static void checkVideo(File inputFile) {
     public static void checkVideo(File inputFile) {
         List<String> command = Arrays.asList(
         List<String> command = Arrays.asList(
-                "ffmpeg", "-v", "error",
+                ffmpeg, "-v", "error",
                 "-i", inputFile.getAbsolutePath(),
                 "-i", inputFile.getAbsolutePath(),
                 "-f", "null", "-"
                 "-f", "null", "-"
         );
         );
 
 
         try {
         try {
-            OutputHandler outputHandler = new EmptyHandler();
-            int exitCode = ShellWrapper.executeFFmpeg(command, outputHandler, outputHandler);
-            if (exitCode != 0) {
-                String errorMsg = "video invalid";
+            ShellResult shellResult = ShellWrapper.executeWithResult(command);
+            if (shellResult.getExitCode() != 0) {
+                String errorMsg = String.format("exec failed");
+                throw new RuntimeException(errorMsg);
+            } else if (!shellResult.getStdout().isEmpty() || !shellResult.getStderr().isEmpty()) {
+                String errorMsg = String.format("video %s invalid", inputFile.getAbsolutePath());
                 throw new RuntimeException(errorMsg);
                 throw new RuntimeException(errorMsg);
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
@@ -96,7 +99,7 @@ public class VideoFileProcessor {
 
 
     public static void convertToWebVideo(File inputFile, File outputFile, double duration) {
     public static void convertToWebVideo(File inputFile, File outputFile, double duration) {
         List<String> command = Arrays.asList(
         List<String> command = Arrays.asList(
-                "ffmpeg", "-y", "-hide_banner",
+                ffmpeg, "-y", "-hide_banner",
                 "-i", inputFile.getAbsolutePath(),
                 "-i", inputFile.getAbsolutePath(),
                 "-c:v", "h264_nvenc",
                 "-c:v", "h264_nvenc",
                 // 1. 替换 -crf 为 NVENC 的质量控制模式
                 // 1. 替换 -crf 为 NVENC 的质量控制模式
@@ -112,7 +115,7 @@ public class VideoFileProcessor {
                 outputFile.getAbsolutePath()
                 outputFile.getAbsolutePath()
         );
         );
         List<String> command1 = Arrays.asList(
         List<String> command1 = Arrays.asList(
-                "ffmpeg", "-y", "-hide_banner",
+                ffmpeg, "-y", "-hide_banner",
                 "-i", inputFile.getAbsolutePath(),
                 "-i", inputFile.getAbsolutePath(),
                 // CPU 上处理滤镜后再交给 GPU 处理
                 // CPU 上处理滤镜后再交给 GPU 处理
                 "-vf", "scale=-2:480",
                 "-vf", "scale=-2:480",
@@ -131,7 +134,7 @@ public class VideoFileProcessor {
         );
         );
 
 
         List<String> command2 = Arrays.asList(
         List<String> command2 = Arrays.asList(
-                "ffmpeg", "-y", "-hide_banner",
+                ffmpeg, "-y", "-hide_banner",
                 "-i", inputFile.getAbsolutePath(),
                 "-i", inputFile.getAbsolutePath(),
                 // 裁剪滤镜:保留中间 1/3
                 // 裁剪滤镜:保留中间 1/3
                 "-vf", "crop=iw/3:ih:iw/3:0",
                 "-vf", "crop=iw/3:ih:iw/3:0",
@@ -151,7 +154,7 @@ public class VideoFileProcessor {
         try {
         try {
             OutputHandler stdoutHandler = new EmptyHandler();
             OutputHandler stdoutHandler = new EmptyHandler();
             OutputHandler stderrHandler = new ConvertVideoOutputHandler(duration);
             OutputHandler stderrHandler = new ConvertVideoOutputHandler(duration);
-            int exitCode = ShellWrapper.executeFFmpeg(command1, stdoutHandler, stderrHandler);
+            int exitCode = ShellWrapper.executeFFmpeg(command, stdoutHandler, stderrHandler);
             if (exitCode != 0) {
             if (exitCode != 0) {
                 String errorMsg = "convert video failed";
                 String errorMsg = "convert video failed";
                 throw new RuntimeException(errorMsg);
                 throw new RuntimeException(errorMsg);
@@ -173,13 +176,13 @@ public class VideoFileProcessor {
 
 
     public static void volumeDetect(File videoFile) {
     public static void volumeDetect(File videoFile) {
         List<String> command = Arrays.asList(
         List<String> command = Arrays.asList(
-                "ffmpeg", "-hide_banner",
+                ffmpeg, "-hide_banner",
                 "-i", videoFile.getAbsolutePath(),
                 "-i", videoFile.getAbsolutePath(),
                 "-af", "volumedetect", "-vn", "-sn", "-f", "null", "/dev/null"
                 "-af", "volumedetect", "-vn", "-sn", "-f", "null", "/dev/null"
         );
         );
 
 
         List<String> command1 = Arrays.asList(
         List<String> command1 = Arrays.asList(
-                "ffmpeg", "-hide_banner",
+                ffmpeg, "-hide_banner",
                 "-i", videoFile.getAbsolutePath(),
                 "-i", videoFile.getAbsolutePath(),
                 "-af", "loudnorm=print_format=summary", "-vn", "-sn", "-f", "null", "/dev/null"
                 "-af", "loudnorm=print_format=summary", "-vn", "-sn", "-f", "null", "/dev/null"
         );
         );
@@ -278,48 +281,4 @@ public class VideoFileProcessor {
         convertToWebVideo(videoFile, outputFile, duration);
         convertToWebVideo(videoFile, outputFile, duration);
         log.info("convert video cost {}s", (System.currentTimeMillis()-start)/1000);*/
         log.info("convert video cost {}s", (System.currentTimeMillis()-start)/1000);*/
     }
     }
-
-    public static void main(String[] args) {
-        String baseDir = "/home/reghao/Downloads/0/2";
-        //baseDir = "/home/reghao/disk/2/porn/zzz/flv/";
-        for (File videoFile : Objects.requireNonNull(new File(baseDir).listFiles())) {
-            //String videoPath = "/home/reghao/Downloads/0/123.flv.mp4";
-            //File videoFile = new File(videoPath);
-            log.info("process video {}", videoFile.getAbsolutePath());
-            //process(videoFile);
-            //volumeDetect(videoFile);
-
-            /*Map<String, Double> metrics = analyzeLoudness(videoFile.getAbsolutePath());
-            if (!metrics.isEmpty()) {
-                System.out.println("--- 响度分析报告 ---");
-                System.out.printf("整体响度 (I): %.2f LUFS\n", metrics.get("input_i"));
-                System.out.printf("真实峰值 (TP): %.2f dB\n", metrics.get("input_tp"));
-                System.out.printf("动态范围 (LRA): %.2f LU\n", metrics.get("input_lra"));
-                // 决策逻辑
-                if (metrics.get("input_i") < -20) {
-                    System.out.println("建议:该视频音量太小,需要增强。");
-                }
-            }*/
-        }
-
-        File inputFile = new File("/home/reghao/Downloads/rtmp.mp4");
-        File outputFile = new File("/home/reghao/Downloads/rtmp.mp4.mp4");
-
-        String filePath = "/home/reghao/Downloads/123abc.mp4";
-        JsonNode meta = getVideoMetadata(inputFile.getAbsolutePath());
-        String durationStr = meta.path("format").path("duration").asText();
-        double duration = durationStr.isEmpty() ? 0.0 : Double.parseDouble(durationStr);
-        convertToWebVideo(inputFile, outputFile, duration);
-
-        String listFile = "list.txt";
-        String outputPath = "output.mp4";
-        List<String> command = Arrays.asList(
-                "ffmpeg", "-y", "-hide_banner",
-                "-f", "concat", "-safe", "0", "-i", listFile,
-                "-c:v", "copy",       // 视频流直接拷贝,速度极快
-                "-c:a", "aac",        // 音频流重新编码,解决时间戳不连续
-                "-b:a", "128k",
-                outputPath
-        );
-    }
 }
 }

+ 67 - 0
oss-store/src/main/java/cn/reghao/oss/store/media/handler/ShellWrapper.java

@@ -157,4 +157,71 @@ public class ShellWrapper {
             return new ShellResult(exitCode, stdout.toString(), stderr.toString());
             return new ShellResult(exitCode, stdout.toString(), stderr.toString());
         }
         }
     }
     }
+
+    public static ShellResult executeWithResult(List<String> commands) throws Exception {
+        ProcessBuilder pb = new ProcessBuilder(commands);
+        Process process = pb.start();
+
+        StringBuilder stdout = new StringBuilder();
+        StringBuilder stderr = new StringBuilder();
+        // 父进程使用两个线程分别读取子进程的 stdout 和 stderr, 也就是子进程会向父进程写数据
+        // FFmpeg 的日志输出在 stderr, Java 程序必须不断读取 process.getErrorStream(), 否则会导致缓冲区满造成进程挂起(Zombie Process)
+        try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
+            executor.submit(() -> {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        if (stdout.length() < 10_000) {
+                            stdout.append(line).append(System.lineSeparator());
+                        }
+                    }
+                } catch (IOException e) {
+                    log.error("读取标准流异常", e);
+                }
+            });
+            executor.submit(() -> {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+                    String line;
+                    // BufferedReader 是同步阻塞式的
+                    // 有数据: 立即读取并返回
+                    // 没数据但流没关: 线程进入等待状态,让出 CPU 资源,直到操作系统通知有新数据到达
+                    // 流关闭时(子进程退出): readLine 返回 null, 此时阻塞解除
+                    while ((line = reader.readLine()) != null) {
+                        if (stderr.length() < 10_000) {
+                            stderr.append(line).append(System.lineSeparator());
+                        }
+                    }
+                } catch (IOException e) {
+                    log.error("读取错误流异常", e);
+                }
+            });
+            // 停止接收新任务,但会把已提交的任务执行完
+            // executor 默认创建的都是非守护
+            executor.shutdown();
+
+            // 执行 kill -9 会让操作系统直接从内存和 CPU 调度中抹除进程, JVM 进程还没来得及执行下一行指令就被杀死,所以 ShutdownHook 自然无法运行
+            // ShutdownHook 依赖于 JVM 接收到操作系统的信号后的内部处理机制
+            // 子进程默认情况下变成"孤儿进程", 它会立即被 1 号进程(init 或 systemd) 领养
+            // 如果子进程正通过 stdin/stdout 与父进程进行实时通信, 父进程被杀时 pipe 关闭, 子进程在下一次尝试向 pipe 写数据时会收到操作系统的 SIGPIPE 信号, 大多数程序在收到 SIGPIPE 时的默认行为是退出, 但如果子进程忽略了该信号或没有写操作那它依然会继续运行
+            // 如果子进程只是在读,那么它收不到 SIGPIPE 信号, 当子进程尝试从 pipe 读取数据, 而写入端(父进程)被关闭时, 读操作会立即返回 0(表示 EOF, 文件结束符), 读取到 EOF 的结果取决于子进程的代码逻辑
+            // 注册钩子:当 JVM 退出时执行
+            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                if (process.isAlive()) {
+                    process.destroyForcibly();
+                    log.error("JVM 退出,已清理残留的 FFmpeg 进程");
+                }
+            }));
+
+            // 1. 设置强制超时,防止 FFmpeg 陷入无限循环
+            if (!process.waitFor(10, TimeUnit.MINUTES)) {
+                // 超过 2 小时还没转完,直接干掉
+                process.destroyForcibly();
+            }
+            // 2. 确保 Java 退出时,FFmpeg 也跟着死
+            process.descendants().forEach(ProcessHandle::destroyForcibly);
+            // 3. 等待进程结束
+            int exitCode = process.waitFor();
+            return new ShellResult(exitCode, stdout.toString(), stderr.toString());
+        }
+    }
 }
 }