|
|
@@ -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);*/
|
|
|
- }
|
|
|
-}
|