java利用ffmpeg截取视频每一帧并保存截取的图片

*要实现使用 Java 调用 FFmpeg 截取视频每一帧并保存图片,我们可以通过 Java 的 ProcessBuilder 来执行 FFmpeg 命令。下面是一个实现该功能的 Java 程序:
本地上传

点击查看代码
package cn.iocoder.boot.video;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
// 在类中添加依赖注入配置的代码
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class VideoFrameExtractor {
    // 从配置文件中注入FFmpeg路径
    @Value("${ffmpeg.path}")
    private String ffmpegPath;

    // 为了保持main方法的独立运行能力,保留无参构造函数
    public VideoFrameExtractor() {
    }

    // 保留原有的构造函数用于测试和独立运行
    public VideoFrameExtractor(String ffmpegPath) {
        this.ffmpegPath = ffmpegPath;
        validateFfmpegPath();
    }
    
    //Spring初始化后验证FFmpeg路径是否有效
    @PostConstruct
    public void validateFfmpegPath() {
        if (ffmpegPath == null || !new File(ffmpegPath).exists()) {
            throw new IllegalArgumentException("FFmpeg executable not found at: " + ffmpegPath);
        }
    }
    
    /**
     * 从视频中提取每一帧并保存为图片
     * @param videoPath 视频文件路径
     * @param outputDir 输出图片目录
     * @param format 输出图片格式 (例如: png, jpg)
     * @throws IOException 如果执行过程中发生错误
     */
    public void extractFrames(String videoPath, String outputDir, String format) throws IOException {
        // 检查视频文件是否存在
        File videoFile = new File(videoPath);
        if (!videoFile.exists()) {
            throw new IllegalArgumentException("Video file not found: " + videoPath);
        }
        
        // 创建输出目录
        File outputDirectory = new File(outputDir);
        if (!outputDirectory.exists()) {
            outputDirectory.mkdirs();
        }
        
        // 构建FFmpeg命令
        // -i: 输入文件
        // -vf fps=1: 每秒提取1帧(修改此值可调整提取频率)
        // %04d: 4位数字序号,从0001开始
        String outputPattern = outputDir + File.separator + "frame_%04d." + format;
        
        ProcessBuilder processBuilder = new ProcessBuilder(
            ffmpegPath,
            "-i", videoPath,
            "-vf", "fps=1",  // 每秒提取1帧,要提取每一帧可改为"fps=60"(根据视频帧率调整)
            "-q:v", "2",     // 图片质量,1-31,越小质量越高
            outputPattern
        );
        
        // 重定向错误流到输入流,便于捕获错误信息
        processBuilder.redirectErrorStream(true);
        
        // 启动进程
        Process process = processBuilder.start();
        
        // 读取进程输出
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);  // 打印FFmpeg输出信息
            }
        }
        
        // 等待进程完成
        try {
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Frames extracted successfully to: " + outputDir);
            } else {
                throw new IOException("FFmpeg process exited with error code: " + exitCode);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Process interrupted", e);
        }
    }
    
    public static void main(String[] args) {
        try {
            // 设置FFmpeg路径(根据实际情况修改)
            // Windows示例
            //String ffmpegPath = args.length > 0 ? args[0] : "..\\\\..\\\\ffmpeg-release-essentials\\\\ffmpeg-8.0-essentials_build\\\\bin\\\\ffmpeg.exe";
            String ffmpegPath = "/usr/local/bin/ffmpeg"; // Linux/Mac示例
            
            VideoFrameExtractor extractor = new VideoFrameExtractor(ffmpegPath);
            
            // 视频文件路径
            String videoPath = "src/main/resources/video/car.mp4";
            
            // 输出目录
            String outputDir = "src/main/resources/picture";
            
            // 提取帧并保存为PNG格式
            extractor.extractFrames(videoPath, outputDir, "png");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


网络上传

点击查看代码
package cn.iocoder.boot.video;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class VideoFrameExtractor {
    // 从配置文件中注入FFmpeg路径
    //Linux环境下的FFmpeg路径
    @Value("${ffmpeg.path:/usr/local/bin/ffmpeg}")
    // Windows环境下的FFmpeg路径
    // @Value("${ffmpeg.path:D:/ffmpeg-release-essentials/ffmpeg-8.0-essentials_build/bin/ffmpeg.exe}")
    private String ffmpegPath;
    // 网络接收端口,可从配置文件注入
    @Value("${video.receive.port:8089 }")
    private int receivePort;
    // 临时文件存储目录
    @Value("${video.temp.dir:/tmp/video-temp}")
    private String tempDir;

    // 新增:客户端连接超时时间(毫秒)
    @Value("${video.client.timeout:30000}")  // 默认30秒
    private int clientTimeout;

    // 新增:支持的视频格式列表
    private static final List<String> SUPPORTED_VIDEO_FORMATS = Arrays.asList(
            "mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "mpeg", "mpg"
    );

    // 为了保持main方法的独立运行能力,保留无参构造函数
    public VideoFrameExtractor() {
    }

    // 保留原有的构造函数用于测试和独立运行
    public VideoFrameExtractor(String ffmpegPath) {
        this.ffmpegPath = ffmpegPath;
        this.receivePort = 8089;
        this.tempDir = "/tmp/video-temp";
        this.clientTimeout = 30000; // 默认30秒
        validateFfmpegPath();
        initTempDirectory();
    }

    // 带参数的构造函数,用于自定义配置
    public VideoFrameExtractor(String ffmpegPath, int receivePort, String tempDir) {
        this.ffmpegPath = ffmpegPath;
        this.receivePort = receivePort;
        this.tempDir = tempDir;
        this.clientTimeout = 30000; // 默认30秒
        validateFfmpegPath();
        initTempDirectory();
    }

    // 添加新构造函数,支持自定义超时
    public VideoFrameExtractor(String ffmpegPath, int receivePort, String tempDir, int clientTimeout) {
        this.ffmpegPath = ffmpegPath;
        this.receivePort = receivePort;
        this.tempDir = tempDir;
        this.clientTimeout = clientTimeout;
        validateFfmpegPath();
        initTempDirectory();
    }

    // Spring初始化后验证FFmpeg路径是否有效并初始化临时目录
    @PostConstruct
    public void init() {
        validateFfmpegPath();
        initTempDirectory();
    }

    // 验证FFmpeg路径
    private void validateFfmpegPath() {
        if (ffmpegPath == null || !new File(ffmpegPath).exists()) {
            throw new IllegalArgumentException("FFmpeg executable not found at: " + ffmpegPath);
        }
    }

    // 初始化临时目录
    private void initTempDirectory() {
        File dir = new File(tempDir);
        if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new RuntimeException("Failed to create temporary directory: " + tempDir);
            }
        }
    }

    /**
     * 启动服务器监听,接收远程视频文件并提取帧
     *
     * @param outputDir 帧输出目录
     * @param format    输出图片格式
     * @throws IOException 发生IO错误时抛出
     */
    public void startFileReceiver(String outputDir, String format) throws IOException {
        ServerSocket serverSocket = null;
        int actualPort = receivePort;

        // 尝试找到可用端口
        while (serverSocket == null) {
            try {
                serverSocket = new ServerSocket(actualPort);
                receivePort = actualPort; // 更新实际使用的端口
                break;
            } catch (java.net.BindException e) {
                System.out.println("Port " + actualPort + " is already in use. Trying next port...");
                actualPort++;
                // 如果尝试了太多端口仍不可用,则抛出异常
                if (actualPort - receivePort > 10) {
                    throw new IOException("Could not find available port after 10 attempts", e);
                }
            }
        }
        // 使用final临时变量(兼容所有Java版本)
        final ServerSocket finalServerSocket = serverSocket;
        try (finalServerSocket) {
            System.out.println("Waiting for video file on port " + receivePort + "...");
            while (true) { // 循环接收多个文件
                try (Socket clientSocket = finalServerSocket.accept()) {
                    System.out.println("Client connected: " + clientSocket.getInetAddress());

                    // 新增:设置客户端连接超时
                    clientSocket.setSoTimeout(clientTimeout);

                    // 保存到临时文件
                    String tempFilePath = saveReceivedFile(clientSocket);
                    if (tempFilePath != null) {
                        // 提取视频帧
                        extractFrames(tempFilePath, outputDir, format);
                        // 处理完成后删除临时文件
                        Files.deleteIfExists(Paths.get(tempFilePath));
                        System.out.println("Temporary file deleted: " + tempFilePath);
                    }
                } catch (SocketTimeoutException e) {
                    System.err.println("Client connection timeout: " + e.getMessage());
                } catch (Exception e) {
                    System.err.println("Error processing file: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }

    // 新增:带格式检测的文件保存方法
    protected String saveReceivedFileWithFormatDetection(Socket socket) throws IOException {
        // 生成唯一ID
        String uniqueId = UUID.randomUUID().toString();
        Path tempFilePath = null;

        try (
                InputStream inputStream = socket.getInputStream();
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)
        ) {
            // 读取文件头信息以检测文件类型
            byte[] headerBuffer = new byte[1024];
            int headerBytesRead = bufferedInputStream.read(headerBuffer);

            if (headerBytesRead == -1) {
                throw new IOException("No data received from client");
            }

            // 检测文件格式
            String detectedFormat = detectVideoFormat(headerBuffer, headerBytesRead);
            if (detectedFormat == null) {
                throw new IOException("Unsupported or unknown video format");
            }

            // 根据检测到的格式创建临时文件
            tempFilePath = Paths.get(tempDir, uniqueId + "." + detectedFormat);

            try (
                    FileOutputStream fileOutputStream = new FileOutputStream(tempFilePath.toString());
                    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)
            ) {
                // 先写入已经读取的文件头
                bufferedOutputStream.write(headerBuffer, 0, headerBytesRead);

                // 继续读取剩余数据
                byte[] buffer = new byte[8192];
                int bytesRead;
                long totalBytesRead = headerBytesRead;

                while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
                    bufferedOutputStream.write(buffer, 0, bytesRead);
                    totalBytesRead += bytesRead;
                }

                bufferedOutputStream.flush();

                System.out.println("Received file: " + tempFilePath + " (" + totalBytesRead + " bytes, format: "
                        + detectedFormat + ")");
            }

            return tempFilePath.toString();
        } catch (IOException e) {
            // 发生错误时删除临时文件
            if (tempFilePath != null) {
                Files.deleteIfExists(tempFilePath);
            }
            throw new IOException("Failed to save received file: " + e.getMessage(), e);
        }
    }

    // 新增:检测视频格式的方法
    private String detectVideoFormat(byte[] headerBuffer, int bytesRead) {
        // 简单的文件格式检测逻辑,基于文件头特征
        if (bytesRead >= 12) {
            // MP4/QuickTime文件头检测
            if (headerBuffer[4] == 'f' && headerBuffer[5] == 't' && headerBuffer[6] == 'y' && headerBuffer[7] == 'p') {
                return "mp4";
            }
            // AVI文件头检测
            if (headerBuffer[0] == 'R' && headerBuffer[1] == 'I' && headerBuffer[2] == 'F' && headerBuffer[3] == 'F' &&
                    headerBuffer[8] == 'A' && headerBuffer[9] == 'V' && headerBuffer[10] == 'I' && headerBuffer[11]
                    == ' ') {
                return "avi";
            }
            // MKV文件头检测
            if (bytesRead >= 16 &&
                    headerBuffer[0] == 0x1A && headerBuffer[1] == 0x45 && headerBuffer[2] == 0xDF && headerBuffer[3]
                    == 0xA3) {
                return "mkv";
            }
            // WebM文件头检测
            if (bytesRead >= 12 &&
                    headerBuffer[0] == 'w' && headerBuffer[1] == 'e' && headerBuffer[2] == 'b' && headerBuffer[3]
                    == 'm') {
                return "webm";
            }
        }

        // 默认返回MP4作为后备选项
        return "mp4";
    }


    // 优化后的保存文件方法
    protected String saveReceivedFile(Socket socket) throws IOException {
        // 生成唯一临时文件名
        String uniqueId = UUID.randomUUID().toString();
        // 直接使用固定扩展名或从客户端获取文件名
        String originalFileName = uniqueId + ".mp4";
        Path tempFilePath = Paths.get(tempDir, originalFileName);

        // 关键优化:使用NIO的FileChannel,直接从网络流写入本地文件,减少内存缓冲
        try (
                InputStream inputStream = socket.getInputStream();
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                FileOutputStream fileOutputStream = new FileOutputStream(tempFilePath.toString());
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)
        ) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            long totalBytesRead = 0;

            // 读取所有数据直到流结束
            while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
                bufferedOutputStream.write(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
            }

            // 确保所有数据都写入磁盘
            bufferedOutputStream.flush();

            // 检查文件大小,确保有数据被写入
            if (totalBytesRead == 0) {
                throw new IOException("No data received from client");
            }

            System.out.println("Received file: " + tempFilePath + " (" + totalBytesRead + " bytes)");

            return tempFilePath.toString();
        } catch (IOException e) {
            // 发生错误时删除临时文件
            Files.deleteIfExists(tempFilePath);
            throw new IOException("Failed to save received file: " + e.getMessage(), e);
        }
    }

    /**
     * 从视频中提取每一帧并保存为图片
     *
     * @param videoPath 视频文件路径
     * @param outputDir 输出图片目录
     * @param format    输出图片格式 (例如: png, jpg)
     * @throws IOException 如果执行过程中发生错误
     */
    public void extractFrames(String videoPath, String outputDir, String format) throws IOException {
        // 检查视频文件是否存在
        File videoFile = new File(videoPath);
        if (!videoFile.exists()) {
            throw new IllegalArgumentException("Video file not found: " + videoPath);
        }

        // 新增:检查文件格式是否支持
        String fileExtension = getFileExtension(videoPath);
        if (!SUPPORTED_VIDEO_FORMATS.contains(fileExtension.toLowerCase())) {
            throw new IllegalArgumentException("Unsupported video format: " + fileExtension + ". Supported formats: "
                    + SUPPORTED_VIDEO_FORMATS);
        }

        // 新增:使用FFmpeg进行文件格式预检查
        if (!isValidVideoFile(videoPath)) {
            throw new IOException("Invalid or corrupted video file: " + videoPath);
        }

        // 创建输出目录
        File outputDirectory = new File(outputDir);
        if (!outputDirectory.exists()) {
            outputDirectory.mkdirs();
        }
        // 构建FFmpeg命令
        String outputPattern = outputDir + File.separator + "frame_%04d." + format;
        ProcessBuilder processBuilder = new ProcessBuilder(
                ffmpegPath,
                "-i", videoPath,
                "-vf", "fps=1",  // 每秒提取1帧,要提取每一帧可改为"fps=60"(根据视频帧率调整)
                "-q:v", "2",     // 图片质量,1-31,越小质量越高
                outputPattern
        );
        // 设置进程工作目录
        processBuilder.directory(outputDirectory);
        // 重定向错误流到输入流,便于捕获错误信息
        processBuilder.redirectErrorStream(true);
        // 启动进程
        Process process = processBuilder.start();
        // 读取进程输出
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);  // 打印FFmpeg输出信息
            }
        }
        // 等待进程完成
        try {
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Frames extracted successfully to: " + outputDir);
            } else {
                throw new IOException("FFmpeg process exited with error code: " + exitCode);
            }
        } catch (
                InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Process interrupted", e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 新增:获取文件扩展名的方法
    private String getFileExtension(String filePath) {
        int lastDotIndex = filePath.lastIndexOf('.');
        if (lastDotIndex > 0 && lastDotIndex < filePath.length() - 1) {
            return filePath.substring(lastDotIndex + 1);
        }
        return "";
    }

    // 新增:使用FFmpeg验证视频文件的方法
    private boolean isValidVideoFile(String videoPath) throws IOException {
        try {
            // 使用FFprobe或FFmpeg检查文件格式
            ProcessBuilder processBuilder = new ProcessBuilder(
                    ffmpegPath,
                    "-v", "error",
                    "-i", videoPath,
                    "-f", "null", "-"
            );

            processBuilder.redirectErrorStream(true);
            Process process = processBuilder.start();

            // 不读取输出,只等待进程完成
            boolean completed = process.waitFor(5, TimeUnit.SECONDS); // 最多等待5秒

            // 获取退出码并检查是否成功
            int exitCode = process.exitValue();
            return completed && exitCode == 0;
        } catch (Exception e) {
            System.err.println("Error validating video file: " + e.getMessage());
            return false;
        }
    }

    public static void main(String[] args) {
        try {
            // Windows环境下的FFmpeg路径
            //String ffmpegPath = "D:\\ffmpeg-release-essentials\\ffmpeg-8.0-essentials_build\\bin\\ffmpeg.exe";

            // Linux环境下的FFmpeg路径 - 尝试多个常见路径
            String ffmpegPath = getValidFfmpegPath();
            if (ffmpegPath == null) {
                System.err.println("FFmpeg executable not found. Please specify a valid path.");
                return;
            }

            // 接收端口
            int port = 8089;
            // 临时文件目录 - 使用更合适的临时目录路径
            String tempDir = System.getProperty("java.io.tmpdir") + File.separator + "video-temp";
            // 客户端超时时间(毫秒)
            int clientTimeout = 30000; // 30秒

            // 确保临时目录存在
            File tempDirFile = new File(tempDir);
            if (!tempDirFile.exists() && !tempDirFile.mkdirs()) {
                System.err.println("Failed to create temporary directory: " + tempDir);
                // 尝试使用当前工作目录作为备选
                tempDir = System.getProperty("user.dir") + File.separator + "temp" + File.separator + "video-temp";
                tempDirFile = new File(tempDir);
                if (!tempDirFile.exists() && !tempDirFile.mkdirs()) {
                    System.err.println("Failed to create fallback temporary directory: " + tempDir);
                    return;
                }
            }

            System.out.println("Using FFmpeg path: " + ffmpegPath);
            System.out.println("Using temporary directory: " + tempDir);

            VideoFrameExtractor extractor = new VideoFrameExtractor(ffmpegPath, port, tempDir, clientTimeout);
            // 帧输出目录 - 确保输出目录存在
            String outputDir = System.getProperty("user.dir") + File.separator + "output_frames";
            File outputDirFile = new File(outputDir);
            if (!outputDirFile.exists() && !outputDirFile.mkdirs()) {
                System.err.println("Failed to create output directory: " + outputDir);
                // 尝试使用当前工作目录作为备选
                outputDir = System.getProperty("user.dir");
                System.out.println("Using current directory as output: " + outputDir);
            }

            // 启动文件接收服务并处理
            extractor.startFileReceiver(outputDir, "png");
        } catch (Exception e) {
            System.err.println("Error in main method: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 查找有效的FFmpeg可执行文件路径
     *
     * @return 有效的FFmpeg路径,如果未找到则返回null
     */
    private static String getValidFfmpegPath() {
        /*
        // Windows系统上常见的FFmpeg路径
        String[] windowsPaths = {
                "D:\\ffmpeg\\bin\\ffmpeg.exe",
                "D:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe",
                "D:\\Program Files (x86)\\ffmpeg\\bin\\ffmpeg.exe"
        };
        // 检查每个路径是否存在且可执行
        for (String path : windowsPaths) {
            if (path != null && new File(path).exists() && new File(path).canExecute()) {
                return path;
            }
        }
        return null;
    }
}
*/

        // Linux系统上常见的FFmpeg路径
        String[] linuxPaths = {
                System.getProperty("ffmpeg.path"), // 从系统属性获取
                "/usr/local/bin/ffmpeg",
                "/usr/bin/ffmpeg",
                "/bin/ffmpeg",
                "/usr/local/ffmpeg/bin/ffmpeg"
        };
        // 检查每个路径是否存在且可执行
        for (String path : linuxPaths) {
            if (path != null && new File(path).exists() && new File(path).canExecute()) {
                return path;
            }
        }

        return null;
    }
}
点击查看代码
spring.application.name=video
#??Windows????FFmpeg??
#ffmpeg.path=D:\\ffmpeg-release-essentials\\ffmpeg-8.0-essentials_build\\bin\\ffmpeg.exe


# FFmpeg????????Linux?????????????
ffmpeg.path=/usr/local/bin/ffmpeg
# ????????
video.receive.port=8089
# ????????
video.temp.dir=/tmp/video-temp
# ?????????????
video.client.timeout=30000
# ????????????????
video.supported.formats=mp4,avi,mov,mkv,wmv,flv,webm,mpeg,mpg

说明:
1.首先需要安装 FFmpeg 并获取其可执行文件路径,程序需要通过此路径调用 FFmpeg
2.代码中的关键参数说明:

  • fps=1:表示每秒提取 1 帧,如果需要提取每一帧,可以根据视频实际帧率调整(如 60fps 视频使用fps=60)
  • -q:v 2:控制输出图片质量,值越小质量越高(范围 1-31)
  • 输出文件名格式frame_%04d.png会生成 frame_0001.png, frame_0002.png 等文件
    3.使用时需要根据操作系统修改ffmpegPath变量:
  • Windows 系统通常为C:/ffmpeg/bin/ffmpeg.exe
  • Linux/Mac 系统通常为/usr/local/bin/ffmpeg

4.运行程序前需要确保:

  • FFmpeg 已正确安装并能在指定路径找到
  • 输入视频文件存在且路径正确
  • 程序有写入输出目录的权限

注意:提取视频的每一帧可能会生成大量图片文件,尤其是对于长视频,会占用较多磁盘空间,请确保有足够的存储空间。
FFmpeg操作命令:

功能 命令 说明
视频提取图片 ffmpeg -i raw.mp4 -r 10 -f image2 -s 140x200 %05d.bmp 指定图片大小参数
ffmpeg -i raw.mp4 -r 10 -f image2 %05d.bmp 没有指定图片大小参数
图片合并视频 ffmpeg -r 10 -f image2 -i %05d.bmp output.mp4

单元测试
下面为 VideoFrameExtractor 类编写一个单元测试,使用 JUnit 5 框架。这个测试会验证程序的基本功能,包括错误处理和正常提取帧的情况。

点击查看代码
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class VideoFrameExtractorTest {

    // 临时目录,测试结束后会自动清理
    @TempDir
    Path tempDir;
    
    private VideoFrameExtractor extractor;
    private String validFfmpegPath;
    private String invalidFfmpegPath;
    private Path testVideoPath;
    private Path outputDir;

    @BeforeEach
    void setUp() throws IOException {
        // 确定有效的FFmpeg路径(根据实际环境修改)
        validFfmpegPath = findFfmpegPath();
        invalidFfmpegPath = tempDir.resolve("invalid_ffmpeg.exe").toString();
        
        // 创建测试输出目录
        outputDir = tempDir.resolve("test_output");
        Files.createDirectory(outputDir);
        
        // 创建一个小型测试视频文件(这里使用一个真实存在的测试视频)
        // 注意:实际测试时需要提供一个真实的短视频文件路径
        testVideoPath = Path.of("src/test/resources/test_video.mp4");
    }

    @AfterEach
    void tearDown() {
        // 清理测试生成的文件(临时目录会自动清理)
    }

    /**
     * 尝试自动查找系统中的FFmpeg路径
     */
    private String findFfmpegPath() {
        // Windows系统
        String windowsPath = "C:/ffmpeg/bin/ffmpeg.exe";
        if (new File(windowsPath).exists()) {
            return windowsPath;
        }
        
        // Linux/Mac系统
        String[] unixPaths = {
            "/usr/local/bin/ffmpeg",
            "/usr/bin/ffmpeg",
            "/opt/homebrew/bin/ffmpeg"
        };
        
        for (String path : unixPaths) {
            if (new File(path).exists()) {
                return path;
            }
        }
        
        // 如果找不到,测试时会跳过需要FFmpeg的测试
        return null;
    }

    /**
     * 测试无效的FFmpeg路径是否会抛出异常
     */
    @Test
    void whenInvalidFfmpegPath_thenThrowException() {
        assertThrows(IllegalArgumentException.class, 
            () -> new VideoFrameExtractor(invalidFfmpegPath));
    }

    /**
     * 测试无效的视频文件路径是否会抛出异常
     */
    @Test
    void whenInvalidVideoPath_thenThrowException() {
        if (validFfmpegPath == null) {
            System.out.println("FFmpeg not found, skipping test");
            return;
        }
        
        extractor = new VideoFrameExtractor(validFfmpegPath);
        String invalidVideoPath = tempDir.resolve("nonexistent_video.mp4").toString();
        
        assertThrows(IllegalArgumentException.class, 
            () -> extractor.extractFrames(invalidVideoPath, outputDir.toString(), "png"));
    }

    /**
     * 测试正常提取视频帧的功能
     */
    @Test
    void whenValidInputs_thenExtractFramesSuccessfully() throws IOException {
        if (validFfmpegPath == null) {
            System.out.println("FFmpeg not found, skipping test");
            return;
        }
        
        // 检查测试视频是否存在
        if (!Files.exists(testVideoPath)) {
            System.out.println("Test video not found at " + testVideoPath + ", skipping test");
            return;
        }
        
        extractor = new VideoFrameExtractor(validFfmpegPath);
        
        // 执行帧提取
        extractor.extractFrames(testVideoPath.toString(), outputDir.toString(), "png");
        
        // 验证输出目录中是否生成了图片文件
        List<Path> imageFiles = Files.list(outputDir)
                .filter(p -> p.getFileName().toString().matches("frame_\\d{4}\\.png"))
                .toList();
        
        assertTrue(imageFiles.size() > 0, "No frames were extracted");
        
        // 验证生成的图片文件是否有效(非空)
        for (Path imageFile : imageFiles) {
            assertTrue(Files.size(imageFile) > 0, "Extracted frame is empty: " + imageFile);
        }
    }

    /**
     * 测试不同的图片格式是否能正常工作
     */
    @Test
    void whenUsingDifferentImageFormats_thenExtractFramesSuccessfully() throws IOException {
        if (validFfmpegPath == null || !Files.exists(testVideoPath)) {
            System.out.println("FFmpeg or test video not found, skipping test");
            return;
        }
        
        extractor = new VideoFrameExtractor(validFfmpegPath);
        String jpgOutputDir = outputDir.resolve("jpg").toString();
        
        // 测试JPG格式
        extractor.extractFrames(testVideoPath.toString(), jpgOutputDir, "jpg");
        
        List<Path> jpgFiles = Files.list(Path.of(jpgOutputDir))
                .filter(p -> p.getFileName().toString().matches("frame_\\d{4}\\.jpg"))
                .toList();
        
        assertTrue(jpgFiles.size() > 0, "No JPG frames were extracted");
    }
}

说明:

1.这个测试需要 JUnit 5 框架支持,确保项目中已添加相关依赖
2测试前需要:

  • 确保系统中安装了 FFmpeg
  • 准备一个测试视频文件(建议使用短时长视频),并放在src/test/resources/test_video.mp4路径下
  • 根据实际环境调整findFfmpegPath()方法中的路径
    3.测试内容包括:
  • 验证无效 FFmpeg 路径的错误处理
  • 验证无效视频文件的错误处理
  • 验证正常情况下能否成功提取帧
  • 验证不同图片格式(PNG、JPG)的支持情况
    4.测试使用了 JUnit 5 的临时目录功能,测试生成的文件会在测试结束后自动清理,不会污染系统
    5.如果找不到 FFmpeg 或测试视频,相关测试会自动跳过而不是失败

另外,可以根据实际需求调整测试用例,例如添加对提取帧率的验证、大文件处理的测试等。

posted @ 2025-09-10 14:39  海洛船长Q  阅读(71)  评论(0)    收藏  举报