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 或测试视频,相关测试会自动跳过而不是失败
另外,可以根据实际需求调整测试用例,例如添加对提取帧率的验证、大文件处理的测试等。
浙公网安备 33010602011771号