reghao пре 3 недеља
родитељ
комит
95a2f4453c

+ 17 - 5
oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java

@@ -1,5 +1,6 @@
 package cn.reghao.oss.api.dto.media;
 
+import cn.reghao.jutil.jdk.media.model.MediaProps;
 import lombok.*;
 
 import java.io.Serializable;
@@ -21,15 +22,26 @@ public class VideoInfo implements Serializable {
     private String audioCodec;
     private Long abitRate;
     private String formatName;
-    private String urlType;
-    private String url;
-    private String quality;
     private Integer width;
     private Integer height;
     // 单位秒
-    private Integer duration;
+    private Double duration;
+    private String urlType;
+    private String url;
+    private String quality;
     private Long size;
-
     //private String videoFileId;
     //private LocalDateTime createTime;
+
+    public VideoInfo(String objectId, MediaProps mediaProps) {
+        this.objectId = objectId;
+        this.videoCodec = mediaProps.getVideoProps().getCodecName();
+        this.vbitRate = mediaProps.getVideoProps().getBitRate();
+        this.audioCodec = mediaProps.getAudioProps() != null ? mediaProps.getAudioProps().getCodecName() : "";
+        this.abitRate = mediaProps.getAudioProps() != null ? mediaProps.getAudioProps().getBitRate() : 0;
+        this.formatName = mediaProps.getFormatName();
+        this.width = mediaProps.getVideoProps().getCodedWidth().intValue();
+        this.height = mediaProps.getVideoProps().getCodedHeight().intValue();
+        this.duration = mediaProps.getVideoProps().getDuration();
+    }
 }

+ 2 - 1
oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java

@@ -1,5 +1,6 @@
 package cn.reghao.oss.api.iface;
 
+import cn.reghao.jutil.jdk.media.model.MediaProps;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.api.dto.media.ImageInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
@@ -17,6 +18,6 @@ public interface StoreService {
     List<DiskVolume> getDiskVolumes();
     String getPartialMd5(String absolutePath, long offset, int length);
     void deleteFile(String absolutePath);
-    VideoInfo getVideoInfo(String objectId, String absolutePath);
+    MediaProps getMediaInfo(String objectId, String absolutePath);
     ImageInfo getImageInfo(String objectId, String absolutePath);
 }

+ 5 - 2
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java

@@ -57,8 +57,11 @@ public class MetadataService {
         UploadPrepareRet uploadPrepareRet;
         long randomOffset = 0;
         if (dataBlock != null) {
-            Random random = new Random(size);
-            randomOffset = (long) (random.nextDouble() * (size - CHUNK_SIZE));
+            if (size > CHUNK_SIZE) {
+                Random random = new Random(size);
+                randomOffset = (long) (random.nextDouble() * (size - CHUNK_SIZE));
+            }
+
             boolean exist = true;
             uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE, exist, randomOffset, CHUNK_SIZE);
         } else {

+ 10 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/OssClientService.java

@@ -1,5 +1,6 @@
 package cn.reghao.oss.mgr.service;
 
+import cn.reghao.jutil.jdk.media.model.*;
 import cn.reghao.jutil.jdk.web.result.Result;
 import cn.reghao.jutil.jdk.web.result.WebResult;
 import cn.reghao.oss.api.constant.ObjectAction;
@@ -304,8 +305,12 @@ public class OssClientService {
 
         StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
         StoreService storeService = rpcService.getStoreService(storeNode);
-        VideoInfo videoInfo = storeService.getVideoInfo(objectId, absolutePath);
+        MediaProps mediaProps = storeService.getMediaInfo(objectId, absolutePath);
+        if (mediaProps.getVideoProps() == null) {
+            throw new RuntimeException(String.format("object with id %s not contain VideoProps", objectId));
+        }
 
+        VideoInfo videoInfo = new VideoInfo(objectId, mediaProps);
         FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
         String objectName = fileMeta.getObjectName();
         int storeNodeId = storeNode.getId();
@@ -314,7 +319,11 @@ public class OssClientService {
         String domain = userNode.getDomain();
         String objectUrl = String.format("//%s/%s", domain, objectName);
         videoInfo.setUrl(objectUrl);
+        videoInfo.setUrlType("mp4");
+        videoInfo.setSize(fileMeta.getSize());
 
+        MediaResolution mediaResolution = MediaQuality.getQuality(videoInfo.getWidth(), videoInfo.getHeight());
+        videoInfo.setQuality(mediaResolution.getQualityStr());
         return videoInfo;
     }
 }

+ 0 - 286
oss-store/src/main/java/cn/reghao/oss/store/media/FFmpegWrapper.java

@@ -1,286 +0,0 @@
-package cn.reghao.oss.store.media;
-
-import cn.reghao.oss.store.media.handler.*;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.extern.slf4j.Slf4j;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.InputStreamReader;
-import java.time.Duration;
-import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @author reghao
- * @date 2026-02-04 16:45:16
- */
-@Slf4j
-public class FFmpegWrapper {
-    private final static String ffprobe = "/usr/bin/ffprobe";
-    private final static String ffmpeg = "/usr/bin/ffmpeg";
-    private static final ObjectMapper mapper = new ObjectMapper();
-
-    /**
-     * 获取视频最详尽的信息:格式、所有流、程序、章节、元数据
-     */
-    public static JsonNode getVideoMetadata(String filePath) {
-        // 构建全量探测命令
-        List<String> cmd = Arrays.asList(
-                ffprobe,
-                "-v", "quiet",           // 不打印日志头
-                "-print_format", "json", // 输出 JSON
-                "-show_format",          // 容器格式信息 (bitrate, size, duration, tags)
-                "-show_streams",         // 所有流 (video, audio, subtitle, data)
-                "-show_chapters",        // 章节信息
-                "-show_programs",        // 节目信息 (常用于 TS 流)
-                "-show_error",           // 如果解析出错,输出错误 JSON
-                filePath
-        );
-
-        try {
-            Process process = new ProcessBuilder(cmd).start();
-            StringBuilder jsonOutput = new StringBuilder();
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    jsonOutput.append(line);
-                }
-            }
-
-            int exitCode = process.waitFor();
-            if (exitCode != 0) {
-                throw new RuntimeException("FFprobe 解析失败,退出码: " + exitCode);
-            }
-
-            return mapper.readTree(jsonOutput.toString());
-        } catch (Exception e) {
-            throw new RuntimeException(e.getMessage());
-        }
-    }
-
-    /**
-     * 辅助方法:快速判断视频是否包含特定类型的流
-     */
-    public static boolean hasStreamType(JsonNode fullMeta, String type) {
-        JsonNode streams = fullMeta.get("streams");
-        if (streams != null && streams.isArray()) {
-            for (JsonNode stream : streams) {
-                if (type.equalsIgnoreCase(stream.path("codec_type").asText())) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    public static void checkVideo(File inputFile) {
-        List<String> command = Arrays.asList(
-                ffmpeg, "-v", "error",
-                "-i", inputFile.getAbsolutePath(),
-                "-f", "null", "-"
-        );
-
-        try {
-            ShellResult shellResult = ShellWrapper.executeWithResult(command);
-            if (shellResult.getExitCode() != 0) {
-                throw new RuntimeException("exec failed");
-            } else if (!shellResult.getStdout().isEmpty() || !shellResult.getStderr().isEmpty()) {
-                String errorMsg = String.format("video %s invalid", inputFile.getAbsolutePath());
-                throw new RuntimeException(errorMsg);
-            }
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public static void convertToWebVideo(File inputFile, File outputFile, double duration) {
-        List<String> command = Arrays.asList(
-                ffmpeg, "-y", "-hide_banner",
-                "-i", inputFile.getAbsolutePath(),
-                "-c:v", "h264_nvenc",
-                // 1. 替换 -crf 为 NVENC 的质量控制模式
-                "-rc", "vbr",         // 启用可变码率控制 (Variable Bit Rate)
-                "-cq", "24",          // 设置目标质量,23-28 是平衡点(数值越小画质越好)
-                "-qmin", "24",        // 限制最小量化值,防止码率过高
-                "-qmax", "24",        // 限制最大量化值,防止画质突降
-                "-preset", "p4",      // NVENC 新版预设为 p1-p7,p4 是 medium,p7 是最慢最高质
-                "-pix_fmt", "yuv420p",
-                "-c:a", "aac",
-                "-b:a", "128k",
-                "-movflags", "+faststart",
-                outputFile.getAbsolutePath()
-        );
-        List<String> command1 = Arrays.asList(
-                ffmpeg, "-y", "-hide_banner",
-                "-i", inputFile.getAbsolutePath(),
-                // CPU 上处理滤镜后再交给 GPU 处理
-                "-vf", "scale=-2:480",
-                "-c:v", "h264_nvenc",
-                // 2. 硬件控制参数
-                "-rc", "vbr",
-                "-cq", "24",          // 缩放到480p后,像素密度降低,24-26通常就足够清晰
-                "-qmin", "24",
-                "-qmax", "24",
-                "-preset", "p4",      // 对应 medium
-                "-pix_fmt", "yuv420p",
-                "-c:a", "aac",
-                "-b:a", "128k",
-                "-movflags", "+faststart",
-                outputFile.getAbsolutePath()
-        );
-
-        List<String> command2 = Arrays.asList(
-                ffmpeg, "-y", "-hide_banner",
-                "-i", inputFile.getAbsolutePath(),
-                // 裁剪滤镜:保留中间 1/3
-                "-vf", "crop=iw/3:ih:iw/3:0",
-                "-c:v", "h264_nvenc",
-                "-rc", "vbr",
-                "-cq", "24",
-                "-qmin", "24",
-                "-qmax", "24",
-                "-preset", "p4",
-                "-pix_fmt", "yuv420p",
-                "-c:a", "aac",
-                "-b:a", "128k",
-                "-movflags", "+faststart",
-                outputFile.getAbsolutePath()
-        );
-
-        try {
-            OutputHandler stdoutHandler = new EmptyHandler();
-            OutputHandler stderrHandler = new ConvertVideoOutputHandler(duration);
-            int exitCode = ShellWrapper.executeFFmpeg(command, stdoutHandler, stderrHandler);
-            if (exitCode != 0) {
-                String errorMsg = "convert video failed";
-                throw new RuntimeException(errorMsg);
-            }
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public static String formatSeconds(double seconds) {
-        // 将 double 转换为 Duration
-        Duration duration = Duration.ofMillis((long) (seconds * 1000));
-        // Java 9+ 提供的便捷方法
-        long hours = duration.toHours();
-        int minutes = duration.toMinutesPart();
-        int secs = duration.toSecondsPart();
-        return String.format("%02d:%02d:%02d", hours, minutes, secs);
-    }
-
-    public static void volumeDetect(File videoFile) {
-        List<String> command = Arrays.asList(
-                ffmpeg, "-hide_banner",
-                "-i", videoFile.getAbsolutePath(),
-                "-af", "volumedetect", "-vn", "-sn", "-f", "null", "/dev/null"
-        );
-
-        List<String> command1 = Arrays.asList(
-                ffmpeg, "-hide_banner",
-                "-i", videoFile.getAbsolutePath(),
-                "-af", "loudnorm=print_format=summary", "-vn", "-sn", "-f", "null", "/dev/null"
-        );
-
-        try {
-            // 匹配任意字符串
-            Pattern stdoutPattern = Pattern.compile(".*");
-            Pattern stderrPattern = Pattern.compile("([^:]+):\\s*([-+]?\\d+\\.?\\d*)");
-            ShellResult shellResult = ShellWrapper.executeWithResult(command1, stdoutPattern, stderrPattern);
-            if (shellResult.getExitCode() == 0) {
-                if (!shellResult.getStderr().isBlank()) {
-                    Map<String, Double> metrics = new HashMap<>();
-                    for (String line : shellResult.getStderr().split(System.lineSeparator())) {
-                        Matcher matcher = stderrPattern.matcher(line);
-                        if (matcher.find()) {
-                            String key = matcher.group(1).trim().toLowerCase().replace(" ", "_");
-                            double value = Double.parseDouble(matcher.group(2));
-                            metrics.put(key, value);
-                        }
-                    }
-
-                    if (!metrics.isEmpty()) {
-                        System.out.println("--- 响度分析报告 ---");
-                        System.out.printf("整体响度 (I): %.2f LUFS\n", metrics.get("input_integrated"));
-                        System.out.printf("真实峰值 (TP): %.2f dBTP\n", metrics.get("input_true_peak"));
-                        System.out.printf("动态范围 (LRA): %.2f LU\n", metrics.get("input_lra"));
-                        System.out.printf("Input Threshold: %.2f LUFS\n", metrics.get("input_threshold"));
-                        // 决策逻辑
-                        if (metrics.get("input_integrated") < -20) {
-                            System.out.println("建议:该视频音量太小,需要增强。");
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public static void main(String[] args) {
-        String path = "";
-        File videoFile = new File(path);
-
-        // 1.检测是否为视频文件
-        // 2.检查视频文件是否损坏
-        // 3.将视频文件转码为可在 web 播放的格式
-        String videoPath = videoFile.getAbsolutePath();
-        JsonNode meta = getVideoMetadata(videoPath);
-        // 1. 检查是否伪装
-        if (!hasStreamType(meta, "video")) {
-            System.out.println("这不是一个视频文件!");
-            return;
-        }
-
-        String durationStr = meta.path("format").path("duration").asText();
-        double duration = durationStr.isEmpty() ? 0.0 : Double.parseDouble(durationStr);
-
-        String videoCodec = "";
-        int width = 0;
-        int height = 0;
-        String audioCodec = "";
-        // 遍历 streams 数组
-        JsonNode streams = meta.path("streams");
-        for (JsonNode stream : streams) {
-            String type = stream.path("codec_type").asText();
-            if ("video".equals(type)) {
-                System.out.println("--- 视频信息 ---");
-                System.out.println("视频编码: " + stream.path("codec_name").asText());
-                System.out.println("分辨率: " + stream.path("width").asInt() + "x" + stream.path("height").asInt());
-                // 视频流可能有自己的码率
-                String vBitrate = stream.path("bit_rate").asText();
-                System.out.println("视频码率: " + (vBitrate.isEmpty() ? "未知" : vBitrate + " bps"));
-            }
-            else if ("audio".equals(type)) {
-                System.out.println("--- 音频信息 ---");
-                System.out.println("音频编码: " + stream.path("codec_name").asText());
-                System.out.println("音频采样率: " + stream.path("sample_rate").asText() + " Hz");
-                System.out.println("音频码率: " + stream.path("bit_rate").asText() + " bps");
-            }
-        }
-
-        // 2. 获取总码率 (Format 级别)
-        //System.out.println("--- 总体信息 ---");
-        JsonNode format = meta.path("format");
-        System.out.println("容器格式: " + format.path("format_name").asText());
-        System.out.println("文件总码率: " + format.path("bit_rate").asText() + " bps");
-        System.out.println("总时长: " + duration + " 秒, " + formatSeconds(duration));
-
-        // 2. 漂亮地打印出整个 JSON(调试用)
-        //System.out.println(meta.toPrettyString());
-
-        /*long start = System.currentTimeMillis();
-        checkVideo(videoFile);
-        log.info("check video cost {}s", (System.currentTimeMillis()-start)/1000);
-
-        String outputPath = String.format("%s.mp4", videoFile.getAbsolutePath());
-        File outputFile = new File(outputPath);
-        start = System.currentTimeMillis();
-        convertToWebVideo(videoFile, outputFile, duration);
-        log.info("convert video cost {}s", (System.currentTimeMillis()-start)/1000);*/
-    }
-}

+ 0 - 148
oss-store/src/main/java/cn/reghao/oss/store/media/VideoProcessor.java

@@ -1,148 +0,0 @@
-package cn.reghao.oss.store.media;
-
-import cn.reghao.oss.store.media.handler.ConvertVideoOutputHandler;
-import cn.reghao.oss.store.media.handler.EmptyHandler;
-import cn.reghao.oss.store.media.handler.OutputHandler;
-import cn.reghao.oss.store.media.handler.ShellWrapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.extern.slf4j.Slf4j;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @author reghao
- * @date 2026-02-04 16:45:16
- */
-@Slf4j
-public class VideoProcessor {
-    static List<Double> getSceneTimestamps(String videoPath) throws Exception {
-        List<Double> timestamps = new ArrayList<>();
-        timestamps.add(0.0); // 默认从0秒开始
-
-        int threshold = 20;
-        // 使用 scdet 滤镜,并将信息输出到 stdout/stderr
-        List<String> cmd = Arrays.asList(
-                "ffmpeg", "-i", videoPath,
-                "-vf", "scdet=threshold=" + threshold,
-                "-f", "null", "-"
-        );
-        ProcessBuilder pb = new ProcessBuilder(cmd);
-        Process process = pb.start();
-        // 读取日志流
-        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
-        String line;
-        while ((line = reader.readLine()) != null) {
-            // 匹配日志行:[parsed_scdet_0 @ 0x...] lavfi.scd.time: 12.456
-            // 在异步读取日志时,正则匹配 "lavfi.scd.time: ([0-9.]+)"
-            // 提取出来的 time 就是场景切换的具体秒数
-            if (line.contains("lavfi.scd.time:")) {
-                String timeStr = line.substring(line.lastIndexOf(":") + 1).trim();
-                timestamps.add(Double.parseDouble(timeStr));
-            }
-        }
-        process.waitFor();
-        return timestamps;
-    }
-
-    static void splitByScenes(String input, List<Double> points) {
-        File file = new File(input);
-        String parentPath = file.getParent();
-        String filename = file.getName();
-        double total = getVideoTotalSeconds(input);
-        for (int i = 0; i < points.size(); i++) {
-            double start = points.get(i);
-            double total0 = 0.0;
-            if (i+1 == points.size()) {
-                total0 = total - start;
-            } else {
-                total0 = points.get(i+1) - start;
-            }
-            // 如果是最后一段,时长由 ffprobe 获取的总长度决定,这里简化演示
-            double duration = (i < points.size() - 1) ? (points.get(i + 1) - start) : -1;
-
-            String output = String.format("%s/%s_part%s.mp4", parentPath, filename, i);
-            // 构造精准剪切命令
-            List<String> command = new ArrayList<>(Arrays.asList(
-                    "ffmpeg", "-hide_banner", "-y",
-                    "-ss", String.valueOf(start),
-                    "-i", input
-            ));
-            if (duration != -1) {
-                command.add("-t");
-                command.add(String.valueOf(duration));
-            }
-            command.addAll(Arrays.asList(
-                    "-c:v", "h264_nvenc",
-                    "-rc", "vbr",
-                    "-cq", "24",
-                    "-qmin", "24",
-                    "-qmax", "24",
-                    "-preset", "p4",
-                    "-pix_fmt", "yuv420p",
-                    "-c:a", "aac",
-                    "-b:a", "128k",
-                    "-movflags", "+faststart",
-                    output
-            ));
-
-            try {
-                OutputHandler stdoutHandler = new EmptyHandler();
-                OutputHandler stderrHandler = new ConvertVideoOutputHandler(total0);
-                int exitCode = ShellWrapper.executeFFmpeg(command, stdoutHandler, stderrHandler);
-                if (exitCode != 0) {
-                    String errorMsg = "convert video failed";
-                    throw new RuntimeException(errorMsg);
-                }
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
-    static double getVideoTotalSeconds(String videoPath) {
-        List<String> cmd = Arrays.asList(
-                "ffprobe",
-                "-v", "error",
-                "-show_entries", "format=duration",
-                "-of", "default=noprint_wrappers=1:nokey=1",
-                videoPath
-        );
-
-        try {
-            Process process = new ProcessBuilder(cmd).start();
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
-                String output = reader.readLine();
-                if (output != null) {
-                    return Double.parseDouble(output.trim());
-                }
-            }
-            process.waitFor();
-        } catch (Exception e) {
-            log.error("获取视频时长失败: {}", videoPath, e);
-        }
-        return 0.0;
-    }
-
-    public static void main(String[] args) throws Exception {
-        String dir = "/home/reghao/disk/3/porn/yyy.待去广告/aaa/1";
-        for (File file : new File(dir).listFiles()) {
-            String absolutePath = file.getAbsolutePath();
-            long start = System.currentTimeMillis();
-            List<Double> list = getSceneTimestamps(file.getAbsolutePath());
-            splitByScenes(absolutePath, list);
-            log.info("process {} cost {}s", absolutePath, (System.currentTimeMillis()-start)/1000);
-        }
-    }
-}

+ 0 - 60
oss-store/src/main/java/cn/reghao/oss/store/media/handler/ConvertVideoOutputHandler.java

@@ -1,60 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @author reghao
- * @date 2026-04-26 13:35:22
- */
-@Slf4j
-public class ConvertVideoOutputHandler implements OutputHandler {
-    static Pattern timePattern = Pattern.compile("time=(\\d{2}:\\d{2}:\\d{2}.\\d{2})");
-    private final double totalSeconds;
-
-    public ConvertVideoOutputHandler(double totalSeconds) {
-        this.totalSeconds = totalSeconds;
-    }
-
-    @Override
-    public void handle(String line) {
-        Matcher matcher = timePattern.matcher(line);
-        if (matcher.find()) {
-            String timeStr = matcher.group(1);
-            double currentSeconds = convertToSeconds(timeStr);
-            double percent = (currentSeconds / totalSeconds) * 100;
-            String percentStr = String.format("%.2f", percent);
-            log.error("FFmpeg ERROR: {}", percentStr);
-            // 限制频率,防止每帧都发消息给前端(比如每增加 1% 发一次)
-            //updateProgressToFrontend(videoId, percent);
-        }
-    }
-
-    /**
-     * 将 FFmpeg 的时间字符串 (HH:mm:ss.SS) 转换为总秒数
-     * @param timeStr 例如 "00:02:16.65"
-     * @return 总秒数,例如 136.65
-     */
-    static double convertToSeconds(String timeStr) {
-        if (timeStr == null || !timeStr.contains(":")) {
-            return 0.0;
-        }
-
-        try {
-            String[] parts = timeStr.split(":");
-            if (parts.length != 3) return 0.0;
-
-            double hours = Double.parseDouble(parts[0]);
-            double minutes = Double.parseDouble(parts[1]);
-            double seconds = Double.parseDouble(parts[2]);
-
-            // 计算总秒数: 时*3600 + 分*60 + 秒
-            return hours * 3600 + minutes * 60 + seconds;
-        } catch (NumberFormatException e) {
-            // 记录异常日志,防止解析错误导致线程崩溃
-            return 0.0;
-        }
-    }
-}

+ 0 - 11
oss-store/src/main/java/cn/reghao/oss/store/media/handler/EmptyHandler.java

@@ -1,11 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-/**
- * @author reghao
- * @date 2026-04-26 13:42:47
- */
-public class EmptyHandler implements OutputHandler {
-    @Override
-    public void handle(String line) {
-    }
-}

+ 0 - 9
oss-store/src/main/java/cn/reghao/oss/store/media/handler/OutputHandler.java

@@ -1,9 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-/**
- * @author reghao
- * @date 2026-04-26 13:34:36
- */
-public interface OutputHandler {
-    void handle(String line);
-}

+ 0 - 18
oss-store/src/main/java/cn/reghao/oss/store/media/handler/ShellResult.java

@@ -1,18 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/**
- * @author reghao
- * @date 2026-04-26 14:45:50
- */
-@NoArgsConstructor
-@AllArgsConstructor
-@Data
-public class ShellResult {
-    private int exitCode;
-    private String stdout;
-    private String stderr;
-}

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

@@ -1,227 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-import lombok.extern.slf4j.Slf4j;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @author reghao
- * @date 2026-04-26 13:33:22
- */
-@Slf4j
-public class ShellWrapper {
-    public static int executeFFmpeg(List<String> commands, OutputHandler stdoutHandler, OutputHandler stderrHandler) 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) {
-                        //log.info("FFmpeg INFO: {}", line);
-                        stdout.append(line).append(System.lineSeparator());
-                        stdoutHandler.handle(line);
-                    }
-                } 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) {
-                        //log.error("FFmpeg ERROR: {}", line);
-                        stderr.append(line).append(System.lineSeparator());
-                        stderrHandler.handle(line);
-                    }
-                } 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();
-
-            ShellResult shellResult = new ShellResult(exitCode, stdout.toString(), stderr.toString());
-            return exitCode;
-        }
-    }
-
-    public static ShellResult executeWithResult(List<String> commands, Pattern stdoutPattern, Pattern stderrPattern) 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) {
-                        //log.info("FFmpeg INFO: {}", line);
-                        Matcher matcher = stdoutPattern.matcher(line);
-                        if (matcher.find()) {
-                            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) {
-                        //log.error("FFmpeg ERROR: {}", line);
-                        Matcher matcher = stderrPattern.matcher(line);
-                        if (matcher.find()) {
-                            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());
-        }
-    }
-
-    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());
-        }
-    }
-}

+ 0 - 27
oss-store/src/main/java/cn/reghao/oss/store/media/handler/SoundOutputHandler.java

@@ -1,27 +0,0 @@
-package cn.reghao.oss.store.media.handler;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @author reghao
- * @date 2026-04-26 13:41:49
- */
-public class SoundOutputHandler implements OutputHandler {
-    // 定义正则表达式匹配 max_volume 和 mean_volume
-    Pattern maxVolPattern = Pattern.compile("max_volume: ([-+]?\\d+\\.?\\d*) dB");
-    Pattern meanVolPattern = Pattern.compile("mean_volume: ([-+]?\\d+\\.?\\d*) dB");
-
-    @Override
-    public void handle(String line) {
-        Matcher maxMatcher = maxVolPattern.matcher(line);
-        if (maxMatcher.find()) {
-            System.out.println(">>> 检测到最大音量: " + maxMatcher.group(1) + " dB");
-        }
-
-        Matcher meanMatcher = meanVolPattern.matcher(line);
-        if (meanMatcher.find()) {
-            System.out.println(">>> 检测到平均音量: " + meanMatcher.group(1) + " dB");
-        }
-    }
-}

+ 5 - 57
oss-store/src/main/java/cn/reghao/oss/store/rpc/StoreServiceImpl.java

@@ -1,15 +1,12 @@
 package cn.reghao.oss.store.rpc;
 
-import cn.reghao.jutil.jdk.media.model.MediaQuality;
-import cn.reghao.jutil.jdk.media.model.MediaResolution;
+import cn.reghao.jutil.jdk.media.FFmpegWrapper;
+import cn.reghao.jutil.jdk.media.model.MediaProps;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.api.dto.media.ImageInfo;
-import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.iface.StoreService;
 import cn.reghao.oss.api.util.OssSamplingHash;
 import cn.reghao.oss.store.disk.DiskService;
-import cn.reghao.oss.store.media.FFmpegWrapper;
-import com.fasterxml.jackson.databind.JsonNode;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.springframework.stereotype.Service;
@@ -29,7 +26,7 @@ import java.util.List;
 @DubboService
 @Service
 public class StoreServiceImpl implements StoreService {
-    private DiskService diskService;
+    private final DiskService diskService;
 
     public StoreServiceImpl(DiskService diskService) {
         this.diskService = diskService;
@@ -55,8 +52,7 @@ public class StoreServiceImpl implements StoreService {
         }
     }
 
-    @Override
-    public VideoInfo getVideoInfo(String objectId, String absolutePath) {
+    public MediaProps getMediaInfo(String objectId, String absolutePath) {
         try {
             File file = new File(absolutePath);
             if (!file.exists()) {
@@ -67,60 +63,12 @@ public class StoreServiceImpl implements StoreService {
                     throw new RuntimeException(errorMsg);
                 }
             }
-
-            log.info("{}", file.getAbsolutePath());
-            JsonNode jsonNode = FFmpegWrapper.getVideoMetadata(file.getAbsolutePath());
-            VideoInfo videoInfo = parseVideoInfo(jsonNode);
-            videoInfo.setObjectId(objectId);
-            videoInfo.setUrlType("mp4");
-            videoInfo.setUrl("");
-
-            MediaResolution mediaResolution = MediaQuality.getQuality(videoInfo.getWidth(), videoInfo.getHeight());
-            videoInfo.setQuality(mediaResolution.getQualityStr());
-            return videoInfo;
+            return FFmpegWrapper.getMediaProps(file.getAbsolutePath());
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
     }
 
-    public VideoInfo parseVideoInfo(JsonNode node) {
-        VideoInfo videoInfo = new VideoInfo();
-        // 1. 处理 format 层级的数据
-        JsonNode format = node.get("format");
-        if (format != null) {
-            videoInfo.setFormatName(format.path("format_name").asText());
-            videoInfo.setSize(format.path("size").asLong());
-            // duration 是字符串 "7.105011",转换为 Integer(秒)
-            videoInfo.setDuration((int) Math.round(format.path("duration").asDouble()));
-        }
-
-        // 2. 处理 streams 层级的数据
-        JsonNode streams = node.get("streams");
-        if (streams != null && streams.isArray()) {
-            for (JsonNode stream : streams) {
-                String codecType = stream.path("codec_type").asText();
-                if ("video".equals(codecType)) {
-                    // 填充视频流信息
-                    videoInfo.setVideoCodec(stream.path("codec_name").asText());
-                    videoInfo.setVbitRate(stream.path("bit_rate").asLong());
-                    videoInfo.setWidth(stream.path("width").asInt());
-                    videoInfo.setHeight(stream.path("height").asInt());
-                } else if ("audio".equals(codecType)) {
-                    // 填充音频流信息
-                    videoInfo.setAudioCodec(stream.path("codec_name").asText());
-                    videoInfo.setAbitRate(stream.path("bit_rate").asLong());
-                }
-            }
-        }
-
-        // 3. 设置业务字段(这些字段不在 JSON 中,需根据你的业务逻辑填充)
-        //videoInfo.setCreateTime(LocalDateTime.now());
-        // videoInfo.setVideoFileId(...);
-        // videoInfo.setQuality(...);
-        // videoInfo.setUrl(...);
-        return videoInfo;
-    }
-
     @Override
     public ImageInfo getImageInfo(String objectId, String absolutePath) {
         try {