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

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

复制代码
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;

public class VideoFrameExtractor {
    
    // FFmpeg可执行文件路径
    private String ffmpegPath;
    
    public VideoFrameExtractor(String ffmpegPath) {
        this.ffmpegPath = ffmpegPath;
        // 验证FFmpeg路径是否有效
        if (!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路径(根据实际情况修改)
            String ffmpegPath = "C:/ffmpeg/bin/ffmpeg.exe"; // Windows示例
            // String ffmpegPath = "/usr/local/bin/ffmpeg"; // Linux/Mac示例
            
            VideoFrameExtractor extractor = new VideoFrameExtractor(ffmpegPath);
            
            // 视频文件路径
            String videoPath = "input_video.mp4";
            
            // 输出目录
            String outputDir = "extracted_frames";
            
            // 提取帧并保存为PNG格式
            extractor.extractFrames(videoPath, outputDir, "png");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

说明:

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 已正确安装并能在指定路径找到
  • 输入视频文件存在且路径正确
  • 程序有写入输出目录的权限

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

下面为 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 或测试视频,相关测试会自动跳过而不是失败

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