SpringBoot整合FFmpeg進行視頻處理的詳細教學
第一部分:認識 FFmpeg
FFmpeg 是什么?想象一下,如果你有一個朋友,他能:
- 把 MP4 變成 AVI,就像把咖啡變成奶茶
- 裁剪視頻,比理發(fā)師剪頭發(fā)還精準
- 提取音頻,比從披薩上分離芝士還干凈
- 壓縮視頻,比你把行李箱塞滿時還高效
這個“萬能朋友”就是 FFmpeg!它是一個開源的聲音/影像處理工具,功能強大到能讓好萊塢特效師失業(yè)(開玩笑的)。
# FFmpeg 的基本心態(tài):
# "給我一個視頻,我能還你一個世界"
# 實際上它想說的是:"ffmpeg -i input.mp4 [一堆參數(shù)] output.mp4"
第二部分:整合步驟 —— 像組裝樂高一樣簡單
步驟1:先給項目來點“開胃菜”—— Maven依賴
<!-- pom.xml -->
<dependencies>
<!-- SpringBoot 標準配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 讓我們記錄FFmpeg的“精彩表演” -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 視頻處理時的“后悔藥”——異常處理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
步驟2:配置FFmpeg —— 像教AI用筷子
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "ffmpeg")
@Data
public class FFmpegConfig {
/**
* FFmpeg可執(zhí)行文件路徑
* Windows: "C:/ffmpeg/bin/ffmpeg.exe"
* Linux/Mac: "/usr/bin/ffmpeg"
*/
private String path;
/**
* 超時時間(秒)
* 防止視頻處理變成“永恒等待”
*/
private Long timeout = 3600L;
/**
* 線程數(shù)
* 多線程就像多雙手,干活更快!
*/
private Integer threads = 4;
}
# application.yml ffmpeg: path: /usr/local/bin/ffmpeg # 你的FFmpeg安裝路徑 timeout: 3600 # 1小時,足夠看一集電視劇了 threads: 4 # 4個線程,四核處理器的最愛
步驟3:創(chuàng)建FFmpeg指揮官
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class FFmpegCommander {
@Autowired
private FFmpegConfig ffmpegConfig;
/**
* 執(zhí)行FFmpeg命令
* @param commands 命令參數(shù)(像給廚師遞菜單)
* @return 是否成功(廚子有沒有把菜做糊)
*/
public boolean execute(List<String> commands) {
List<String> fullCommand = new ArrayList<>();
fullCommand.add(ffmpegConfig.getPath());
fullCommand.addAll(commands);
log.info("FFmpeg開始干活啦!命令:{}", String.join(" ", fullCommand));
ProcessBuilder processBuilder = new ProcessBuilder(fullCommand);
processBuilder.redirectErrorStream(true); // 錯誤輸出也給我看看
try {
Process process = processBuilder.start();
// 讀取輸出,防止FFmpeg“自言自語”沒人聽
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.debug("FFmpeg悄悄說:{}", line);
}
}
// 等待處理完成,別急著催
int exitCode = process.waitFor();
boolean success = exitCode == 0;
if (success) {
log.info("FFmpeg完美收工!");
} else {
log.error("FFmpeg罷工了!退出碼:{}", exitCode);
}
return success;
} catch (Exception e) {
log.error("FFmpeg崩潰了,原因:{}", e.getMessage(), e);
return false;
}
}
/**
* 獲取FFmpeg版本(驗明正身)
*/
public String getVersion() {
try {
Process process = new ProcessBuilder(ffmpegConfig.getPath(), "-version").start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
return reader.readLine(); // 第一行就是版本信息
} catch (Exception e) {
return "FFmpeg可能去度假了:" + e.getMessage();
}
}
}
步驟4:創(chuàng)建視頻處理服務 —— 你的私人視頻管家
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Slf4j
@Service
public class VideoService {
@Autowired
private FFmpegCommander ffmpegCommander;
// 臨時文件存放目錄(像快遞的臨時存放點)
private final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/video-process/";
public VideoService() {
// 確保臨時目錄存在
new File(TEMP_DIR).mkdirs();
}
/**
* 轉(zhuǎn)換視頻格式(像把中文翻譯成英文)
* @param inputFile 輸入文件
* @param targetFormat 目標格式(mp4, avi, mov...)
*/
public File convertFormat(MultipartFile inputFile, String targetFormat) throws IOException {
log.info("開始格式轉(zhuǎn)換:{} → {}",
getFileExtension(inputFile.getOriginalFilename()),
targetFormat);
// 1. 保存上傳的文件(像把食材先放到廚房)
File input = saveTempFile(inputFile);
// 2. 準備輸出文件(準備好盤子)
String outputFileName = UUID.randomUUID() + "." + targetFormat;
File output = new File(TEMP_DIR + outputFileName);
// 3. 構建FFmpeg命令菜單
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(), // 輸入文件
"-threads", "4", // 用4個線程
"-preset", "fast", // 快速預設
"-c:v", "libx264", // 視頻編碼
"-c:a", "aac", // 音頻編碼
"-y", // 覆蓋輸出文件(別問我是否確定)
output.getAbsolutePath() // 輸出文件
);
// 4. 讓FFmpeg大廚開始烹飪
boolean success = ffmpegCommander.execute(commands);
// 5. 清理臨時文件(洗盤子)
input.delete();
if (success && output.exists()) {
log.info("格式轉(zhuǎn)換成功!文件大?。簕} MB",
output.length() / (1024 * 1024));
return output;
} else {
throw new RuntimeException("轉(zhuǎn)換失敗,F(xiàn)Fmpeg可能去做美甲了");
}
}
/**
* 提取視頻縮略圖(給視頻拍證件照)
*/
public File extractThumbnail(MultipartFile videoFile, int second) throws IOException {
log.info("正在給視頻拍第{}秒的證件照...", second);
File input = saveTempFile(videoFile);
String outputFileName = UUID.randomUUID() + ".jpg";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(),
"-ss", String.valueOf(second), // 跳轉(zhuǎn)到指定秒數(shù)
"-vframes", "1", // 只要1幀
"-vf", "scale=320:-1", // 縮放到寬度320,高度自動
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
input.delete();
if (success && output.exists()) {
log.info("縮略圖生成成功!");
return output;
}
throw new RuntimeException("拍照失敗,視頻可能害羞了");
}
/**
* 壓縮視頻(給視頻減肥)
*/
public File compressVideo(MultipartFile videoFile, int targetBitrate) throws IOException {
log.info("開始給視頻減肥,目標比特率:{}k", targetBitrate);
File input = saveTempFile(videoFile);
long originalSize = input.length();
String outputFileName = UUID.randomUUID() + "_compressed.mp4";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(),
"-threads", "4",
"-b:v", targetBitrate + "k", // 目標視頻比特率
"-b:a", "128k", // 音頻比特率
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
input.delete();
if (success && output.exists()) {
long compressedSize = output.length();
double ratio = (1.0 - (double)compressedSize/originalSize) * 100;
log.info("減肥成功!原大小:{}MB,現(xiàn)大?。簕}MB,瘦身:{:.1f}%",
originalSize/(1024*1024),
compressedSize/(1024*1024),
ratio);
return output;
}
throw new RuntimeException("減肥失敗,視頻可能偷吃宵夜了");
}
/**
* 合并視頻和音頻(像給電影配音)
*/
public File mergeVideoAudio(MultipartFile videoFile,
MultipartFile audioFile) throws IOException {
log.info("開始給視頻配音...");
File video = saveTempFile(videoFile);
File audio = saveTempFile(audioFile);
String outputFileName = UUID.randomUUID() + "_merged.mp4";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", video.getAbsolutePath(),
"-i", audio.getAbsolutePath(),
"-c:v", "copy", // 視頻流直接復制(不重新編碼)
"-c:a", "aac", // 音頻重新編碼
"-map", "0:v:0", // 取第一個文件的視頻
"-map", "1:a:0", // 取第二個文件的音頻
"-shortest", // 以最短的流為準
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
video.delete();
audio.delete();
if (success && output.exists()) {
log.info("配音成功!新視頻誕生了");
return output;
}
throw new RuntimeException("合并失敗,可能視頻和音頻在鬧離婚");
}
private File saveTempFile(MultipartFile file) throws IOException {
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get(TEMP_DIR + fileName);
Files.copy(file.getInputStream(), path);
return path.toFile();
}
private String getFileExtension(String filename) {
if (filename == null) return "unknown";
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1);
}
}
步驟5:創(chuàng)建控制器 —— 視頻處理的接待處
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private FFmpegCommander ffmpegCommander;
@GetMapping("/version")
public String getFFmpegVersion() {
String version = ffmpegCommander.getVersion();
return "{\"version\": \"" + version + "\"}";
}
@PostMapping("/convert")
public ResponseEntity<Resource> convertFormat(
@RequestParam("file") MultipartFile file,
@RequestParam("format") String format,
HttpServletResponse response) throws IOException {
log.info("收到轉(zhuǎn)換請求:{} → {}", file.getOriginalFilename(), format);
File converted = videoService.convertFormat(file, format);
return buildFileResponse(converted,
"converted." + format,
MediaType.APPLICATION_OCTET_STREAM);
}
@PostMapping("/thumbnail")
public ResponseEntity<Resource> extractThumbnail(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "second", defaultValue = "5") int second) throws IOException {
File thumbnail = videoService.extractThumbnail(file, second);
return buildFileResponse(thumbnail,
"thumbnail.jpg",
MediaType.IMAGE_JPEG);
}
@PostMapping("/compress")
public ResponseEntity<Resource> compressVideo(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "bitrate", defaultValue = "1000") int bitrate) throws IOException {
File compressed = videoService.compressVideo(file, bitrate);
return buildFileResponse(compressed,
"compressed.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
@PostMapping("/merge")
public ResponseEntity<Resource> mergeVideoAudio(
@RequestParam("video") MultipartFile video,
@RequestParam("audio") MultipartFile audio) throws IOException {
File merged = videoService.mergeVideoAudio(video, audio);
return buildFileResponse(merged,
"merged.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
private ResponseEntity<Resource> buildFileResponse(File file,
String filename,
MediaType mediaType) {
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
// 文件下載完成后自動刪除(深藏功與名)
file.deleteOnExit();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.contentType(mediaType)
.contentLength(file.length())
.body(resource);
}
}
步驟6:添加異常處理 —— 給程序買份保險
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
log.error("系統(tǒng)鬧情緒了:{}", e.getMessage(), e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "服務器開小差了,可能是FFmpeg在偷懶");
response.put("error", e.getMessage());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxSizeException() {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "文件太大了,服務器拿不動了");
response.put("suggestion", "請嘗試壓縮視頻或上傳小一點的文件");
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(response);
}
@ExceptionHandler(IOException.class)
public ResponseEntity<Map<String, Object>> handleIOException(IOException e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "文件讀寫出了問題,可能是磁盤在鬧脾氣");
response.put("error", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
第三部分:使用示例 —— 讓我們來實際操練一下
1. 啟動應用程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class VideoProcessingApplication {
public static void main(String[] args) {
SpringApplication.run(VideoProcessingApplication.class, args);
System.out.println("視頻處理服務啟動成功!");
System.out.println("FFmpeg整裝待發(fā),隨時準備處理你的視頻");
}
}
2. 測試API
使用Postman或curl測試:
# 查看FFmpeg版本
curl http://localhost:8080/api/video/version
# 轉(zhuǎn)換視頻格式
curl -X POST -F "file=@input.avi" -F "format=mp4" \
http://localhost:8080/api/video/convert --output output.mp4
# 提取縮略圖
curl -X POST -F "file=@video.mp4" -F "second=10" \
http://localhost:8080/api/video/thumbnail --output thumbnail.jpg
# 壓縮視頻
curl -X POST -F "file=@large_video.mp4" -F "bitrate=500" \
http://localhost:8080/api/video/compress --output compressed.mp4
第四部分:高級技巧 —— 讓FFmpeg更懂你
1. 添加進度監(jiān)聽(給視頻處理加個進度條)
public interface ProgressListener {
void onProgress(double percentage, String message);
void onComplete(File outputFile);
void onError(String error);
}
// 在FFmpegCommander中添加進度解析
private void parseProgress(String line, ProgressListener listener) {
// 解析FFmpeg的輸出,提取進度信息
// 示例輸出:frame= 123 fps=25.1 time=00:00:04.92 bitrate= 512.0kbits/s
if (line.contains("time=")) {
// 這里可以解析時間,計算進度百分比
// 實際實現(xiàn)需要根據(jù)視頻總時長計算
}
}
2. 批量處理(一次處理多個文件)
public List<File> batchConvert(List<MultipartFile> files, String format) {
return files.parallelStream() // 并行處理,更快!
.map(file -> {
try {
return videoService.convertFormat(file, format);
} catch (IOException e) {
log.error("轉(zhuǎn)換失?。簕}", file.getOriginalFilename(), e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
3. 視頻信息提取(給視頻做體檢)
public Map<String, Object> getVideoInfo(File videoFile) {
// 使用FFprobe(FFmpeg的小伙伴)獲取視頻信息
List<String> commands = Arrays.asList(
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height,duration,bit_rate,codec_name",
"-of", "json",
videoFile.getAbsolutePath()
);
// 執(zhí)行命令并解析JSON結果
// 返回包含分辨率、時長、碼率、編碼格式等信息
}
第五部分:總結與注意事項
成功整合的秘訣:
正確安裝FFmpeg:確保系統(tǒng)PATH中有FFmpeg,或者配置正確的路徑
# 檢查安裝 ffmpeg -version
資源管理:
java -Xmx2g -jar your-application.jar
- 視頻處理很吃內(nèi)存,記得給JVM足夠的內(nèi)存
- 及時清理臨時文件,防止磁盤被撐爆
錯誤處理:
- FFmpeg可能會因為各種原因失敗(不支持的格式、損壞的文件等)
- 添加重試機制和詳細的日志記錄
安全性:
- 限制上傳文件類型和大小
- 對用戶輸入進行嚴格驗證
- 防止命令注入攻擊
可能遇到的坑
- 跨平臺問題:Windows和Linux下的路徑差異
- 編碼問題:中文字符在命令中可能需要特殊處理
- 權限問題:確保應用有執(zhí)行FFmpeg的權限
- 性能問題:大文件處理可能需要很長時間,考慮異步處理
為什么選擇這個方案?
- 靈活性強:可以執(zhí)行任何FFmpeg支持的操作
- 功能全面:視頻處理界的"瑞士軍刀"
- 社區(qū)支持好:遇到問題容易找到解決方案
- 免費開源:省錢又省心
最后
FFmpeg就像一把強大的電鋸——功能強大但需要小心使用。不要在生產(chǎn)環(huán)境直接運行未經(jīng)驗證的命令,否則可能會:
- 把服務器CPU燒得像烤紅薯
- 讓磁盤空間消失得比錢包里的錢還快
- 產(chǎn)生一堆讓你懷疑人生的臨時文件
但只要你按照本文的步驟,像對待一只溫順的貓一樣對待FFmpeg,它就會成為你在視頻處理領域最得力的助手!
以上就是SpringBoot整合FFmpeg進行視頻處理的詳細教學的詳細內(nèi)容,更多關于SpringBoot FFmpeg視頻處理的資料請關注腳本之家其它相關文章!
相關文章
JavaWeb?Servlet實現(xiàn)文件上傳與下載功能實例
因自己負責的項目中需要實現(xiàn)文件上傳,所以下面下面這篇文章主要給大家介紹了關于JavaWeb?Servlet實現(xiàn)文件上傳與下載功能的相關資料,需要的朋友可以參考下2022-04-04
Spring系列中的beanFactory與ApplicationContext
這篇文章主要介紹了Spring系列中的beanFactory與ApplicationContext,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09
Java 實戰(zhàn)項目錘煉之醫(yī)院門診收費管理系統(tǒng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+html+jdbc+mysql實現(xiàn)一個醫(yī)院門診收費管理系統(tǒng),大家可以在過程中查缺補漏,提升水平2021-11-11
Java實現(xiàn)讀取CSV文件并將數(shù)據(jù)放入對象
CSV文件是一種常見的數(shù)據(jù)存儲格式,廣泛應用于數(shù)據(jù)交換,日志記錄和表格數(shù)據(jù)處理,在Java開發(fā)中,讀取CSV文件并將數(shù)據(jù)映射到對象中是常見的需求,本文將詳細介紹幾種常見方法,有需要的可以了解下2025-06-06

