package com.doumee.service.business.collection; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; /** * 从视频指定时刻截取 JPEG 帧。 */ @Slf4j public final class MediaFrameUtil { private static final long SNAPSHOT_TIMEOUT_MINUTES = 10; private MediaFrameUtil() { } public static double probeDurationSec(String ffmpegPath, File source) { if (source == null || !source.exists() || source.length() <= 0) { return 0; } String ffprobe = resolveFfprobe(ffmpegPath); List command = Arrays.asList( ffprobe, "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", source.getAbsolutePath() ); try { ProcessBuilder builder = new ProcessBuilder(command); builder.redirectErrorStream(true); Process process = builder.start(); String output = readStream(process.getInputStream()); boolean finished = process.waitFor(2, TimeUnit.MINUTES); if (!finished) { process.destroyForcibly(); return 0; } if (process.exitValue() != 0 || StringUtils.isBlank(output)) { return 0; } return Double.parseDouble(output.trim()); } catch (Exception e) { log.warn("ffprobe 读取时长失败: {}", e.getMessage()); return 0; } } public static boolean extractFrame(String ffmpegPath, File source, File target, double second) { if (source == null || !source.exists() || source.length() <= 0) { return false; } if (second < 0) { second = 0; } String ffmpeg = resolveExecutable(ffmpegPath, "ffmpeg"); String sec = formatSeconds(second); List command = Arrays.asList( ffmpeg, "-y", "-ss", sec, "-i", source.getAbsolutePath(), "-frames:v", "1", "-q:v", "2", target.getAbsolutePath() ); try { ProcessBuilder builder = new ProcessBuilder(command); builder.redirectErrorStream(true); Process process = builder.start(); readStream(process.getInputStream()); boolean finished = process.waitFor(SNAPSHOT_TIMEOUT_MINUTES, TimeUnit.MINUTES); if (!finished) { process.destroyForcibly(); log.error("FFmpeg 截帧超时 source={} sec={}", source.getAbsolutePath(), sec); return false; } if (process.exitValue() != 0) { log.error("FFmpeg 截帧失败 exitCode={} source={} sec={}", process.exitValue(), source.getAbsolutePath(), sec); return false; } return target.exists() && target.length() > 0; } catch (Exception e) { log.error("FFmpeg 截帧异常 source={} sec={}: {}", source.getAbsolutePath(), sec, e.getMessage()); return false; } } private static String formatSeconds(double second) { if (second <= 0) { return "0"; } return String.format(Locale.US, "%.3f", second); } private static String readStream(java.io.InputStream in) throws Exception { StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); } } return sb.toString(); } private static String resolveExecutable(String configuredPath, String defaultName) { if (StringUtils.isNotBlank(configuredPath)) { return configuredPath.trim(); } return defaultName; } private static String resolveFfprobe(String ffmpegPath) { if (StringUtils.isBlank(ffmpegPath)) { return "ffprobe"; } String path = ffmpegPath.trim(); if (path.contains("/") || path.contains("\\")) { int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (slash >= 0) { String dir = path.substring(0, slash + 1); String name = path.substring(slash + 1); if (name.toLowerCase().endsWith(".exe")) { return dir + name.replaceAll("(?i)ffmpeg\\.exe$", "ffprobe.exe"); } return dir + name.replaceAll("(?i)ffmpeg$", "ffprobe"); } return path.replaceAll("(?i)ffmpeg", "ffprobe"); } return "ffprobe"; } }