SpringBoot基于FFmpeg實(shí)現(xiàn)壓縮視頻切片為m3u8
分享一個關(guān)于使用ffmpeg對mp4文件進(jìn)行壓縮切片為hls格式m3u8文件的命令行調(diào)用程序。
前提是已經(jīng)安裝了ffmpeg,安裝過程就不再贅述了。
直接上代碼
package xxxxx;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import com.czi.uavcloud.common.utils.StringUtils;
import com.czi.uavcloud.fileprocess.utils.FileUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;
/**
* 壓縮切片處理器
*
* @Author god_cvz
*/
@Slf4j
public class VideoCompressHandle {
protected static final boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");
protected static final String SLASH = WINDOWS ? "\\" : "/";
// 輸出文件路徑,可以自行修改
public static final String TEMP_FOLDER_PATH = (WINDOWS ? "D:" : SLASH + "var") + SLASH + "tempVideoCompress" + SLASH;
// 切片時長,默認(rèn)5秒一個切片
private static final String DEFAULT_HLS_TIME = "5";
/**
* 從指定URL下載視頻并壓縮切片為m3u8
*
* @param downloadUrl 視頻下載地址
*/
public static void compressWithUrl(String downloadUrl) {
String videoFilePath = TEMP_FOLDER_PATH + UUID.randomUUID() + SLASH + FileUtil.getName(downloadUrl);
// 下載視頻至指定路徑
try (BufferedInputStream bufferInput = new BufferedInputStream(getInputStreamFromUrl(downloadUrl));
FileOutputStream out = new FileOutputStream(videoFilePath)) {
IoUtil.copy(bufferInput, out);
} catch (Exception e) {
e.printStackTrace();
}
compressWithLocal(videoFilePath);
}
/**
* 從本地路徑讀取視頻并壓縮切片為m3u8
*
* @param videoFilePath 本地視頻地址
*/
public static void compressWithLocal(String videoFilePath) {
// hutool工具返回的名稱是帶后綴的,入http://abc/123.mp4,返回的是123.mp4
String videoName = FileUtil.getName(videoFilePath);
// 創(chuàng)建臨時文件夾目錄
String tempFolderPath = TEMP_FOLDER_PATH + UUID.randomUUID();
FileUtil.mkdir(tempFolderPath);
if (!WINDOWS) {
try {
// 文件夾授權(quán)
asyncExecute(new String[]{"chmod", "777", "-R", tempFolderPath});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// m3u8文件輸出地址:/var/tempVideoCompress/業(yè)務(wù)id/文件名/文件名.m3u8
String outputFolderPath = tempFolderPath + SLASH + videoName.split("\\.")[0];
FileUtil.mkdir(outputFolderPath);
String m3u8FilePath = outputFolderPath + SLASH + videoName.split("\\.")[0] + ".m3u8";
handle(tempFolderPath, videoFilePath, m3u8FilePath);
}
/**
* 從指定 URL 下載資源并返回 InputStream
*
* @param urlString 資源的 URL 地址
* @return InputStream (需要調(diào)用方手動關(guān)閉)
* @throws IOException 下載或連接失敗時拋出異常
*/
public static InputStream getInputStreamFromUrl(String urlString) throws Exception {
if (urlString == null || urlString.isEmpty()) {
throw new IllegalArgumentException("URL 不能為空");
}
log.info("[Download] 開始下載文件:{}", urlString);
long startTime = System.currentTimeMillis();
try {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10_000);
connection.setReadTimeout(15_000);
connection.setDoInput(true);
// 檢查響應(yīng)碼
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("下載失敗,HTTP 響應(yīng)碼:" + responseCode);
}
log.info("[Download] 連接成功[{}],開始接收數(shù)據(jù)...", url);
InputStream inputStream = connection.getInputStream();
long endTime = System.currentTimeMillis();
log.info("[Download] 下載完成,用時 {} ms", (endTime - startTime));
return inputStream;
} catch (Exception e) {
log.error("[Download] 下載文件失敗[{}], error:{}", urlString, e.getMessage());
throw new Exception("[Download] 下載文件失敗: " + e.getMessage());
}
}
public static void handle(String tempFolderPath, String videoFilePath, String m3u8FilePath) {
long startTime = System.currentTimeMillis();
try {
// 壓縮切片
log.info("[視頻壓縮切片]壓縮切片文件");
ffmpegCompress(videoFilePath, m3u8FilePath);
// 需要上傳的可以自己處理
// log.info("[視頻壓縮切片]上傳切片目錄文件:{}", videoFilePath);
// uploadFolder(outputFolder);
} catch (Exception e) {
log.info("[視頻壓縮切片]處理失敗:{}", e.getMessage());
} finally {
long costTime = System.currentTimeMillis() - startTime;
log.info("[視頻壓縮切片]總耗時:{}", costTime);
// log.info("[視頻壓縮切片]刪除本地壓縮包,釋放空間:{}", tempFolderPath);
// FileUtil.del(tempFolderPath);
}
}
/**
* 將mp4視頻文件轉(zhuǎn)碼壓縮為m3u8并切片
*
* @param inputFile
* @param outputFile
*/
public static void ffmpegCompress(String inputFile, String outputFile) {
ffmpegCompress(inputFile, outputFile, DEFAULT_HLS_TIME);
}
/**
* 將mp4視頻文件轉(zhuǎn)碼壓縮為m3u8并切片
*
* @param inputFile 待處理文件
* @param outputFile 輸出的具體文件路徑
*/
public static void ffmpegCompress(String inputFile, String outputFile, String hlsTime) {
// 上級目錄絕對路徑
String absoluteFolderPath = outputFile.substring(0, outputFile.indexOf(FileUtil.getName(outputFile)));
if (!FileUtil.exist(absoluteFolderPath)) {
FileUtils.createDirIfAbsent(absoluteFolderPath);
}
log.info("開始壓縮轉(zhuǎn)碼文件:in:{},out:{},hlsTime:{}", inputFile, outputFile, hlsTime);
long startTime = System.currentTimeMillis();
// linux的ffmpeg可執(zhí)行文件放在/usr/bin/ffmpeg,可以自行修改
String cmd = WINDOWS ? "D:\\ffmpeg\\bin\\ffmpeg.exe" : "/usr/bin/ffmpeg";
String[] command = new String[]{cmd,
"-i", inputFile,
//多線程數(shù)
"-threads", "2",
//幀數(shù)
"-r", "25",
//碼率
"-b:v", "3000k",
//分辨率
"-s", "1920x1080",
//ultrafast(轉(zhuǎn)碼速度最快,視頻往往也最模糊)、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo這10個選項(xiàng),從快到慢
"-preset", "ultrafast",
//視頻畫質(zhì)級別 1-5
"-level", "3.0",
//從0開始
"-start_number", "0",
"-g", "50",
//設(shè)置編碼器
"-codec:v", "h264",
//轉(zhuǎn)碼
"-f", "hls",
//每5秒切一個
"-hls_time", StringUtils.isNotBlank(hlsTime) ? hlsTime : DEFAULT_HLS_TIME,
//設(shè)置播放列表保存的最多條目,設(shè)置為0會保存所有切片信息,默認(rèn)值為5
"-hls_list_size", "0",
outputFile};
try {
asyncExecute(command);
} catch (IOException e) {
throw new RuntimeException(e);
}
log.info("壓縮轉(zhuǎn)碼文件完成:{},耗時:{}", outputFile, System.currentTimeMillis() - startTime);
}
/**
* 執(zhí)行shell命令
*
* @param cmd
*/
public static Integer asyncExecute(String[] cmd) throws IOException {
return asyncExecute(cmd, null, null);
}
/**
* 執(zhí)行shell命令(支持任務(wù)管理器)
*
* @param cmd 命令數(shù)組
* @param taskManager 任務(wù)管理器,用于注冊進(jìn)程和檢查取消狀態(tài)
* @param taskId 任務(wù)ID
*/
public static Integer asyncExecute(String[] cmd, Object taskManager, Long taskId) throws IOException {
log.info("執(zhí)行命令:{}", String.join(" ", cmd));
Process process = new ProcessBuilder(cmd)
// 合并錯誤流和標(biāo)準(zhǔn)流
.redirectErrorStream(true)
.start();
// 如果有任務(wù)管理器,注冊進(jìn)程
if (taskManager != null && taskId != null) {
try {
// 使用反射調(diào)用 registerChildProcess 方法
taskManager.getClass().getMethod("registerChildProcess", Long.class, Process.class)
.invoke(taskManager, taskId, process);
} catch (Exception e) {
log.warn("注冊子進(jìn)程失敗: {}", e.getMessage());
}
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// 異步讀取輸出(防止阻塞)
Thread outputThread = new Thread(() -> {
try {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[PROCESS] " + line);
// 檢查任務(wù)是否被取消
if (taskManager != null && taskId != null) {
try {
Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
.invoke(taskManager, taskId);
if (context != null) {
Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
.invoke(context);
if (shouldStop != null && shouldStop) {
log.info("檢測到任務(wù)取消,停止讀取進(jìn)程輸出,任務(wù)ID: {}", taskId);
break;
}
}
} catch (Exception e) {
// 忽略反射調(diào)用異常
}
}
}
} catch (IOException e) {
System.err.println("輸出流讀取異常: " + e.getMessage());
}
});
outputThread.start();
// 等待進(jìn)程完成,支持中斷檢查
int exitCode = waitForProcessWithCancellationCheck(process, taskManager, taskId);
// 等待輸出線程結(jié)束
outputThread.join(1000);
return exitCode;
} catch (InterruptedException e) {
log.info("進(jìn)程執(zhí)行被中斷");
process.destroyForcibly();
throw new RuntimeException("進(jìn)程執(zhí)行被取消", e);
}
}
/**
* 等待進(jìn)程完成,支持取消檢查
*/
private static int waitForProcessWithCancellationCheck(Process process, Object taskManager, Long taskId) throws InterruptedException {
while (true) {
try {
// 檢查線程是否被中斷
if (Thread.currentThread().isInterrupted()) {
log.info("檢測到線程中斷,正在終止進(jìn)程...");
process.destroyForcibly();
throw new InterruptedException("任務(wù)被取消");
}
// 檢查任務(wù)管理器中的任務(wù)狀態(tài)
if (taskManager != null && taskId != null) {
try {
Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
.invoke(taskManager, taskId);
if (context != null) {
Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
.invoke(context);
if (shouldStop != null && shouldStop) {
log.info("檢測到任務(wù)取消請求,正在終止進(jìn)程,任務(wù)ID: {}", taskId);
process.destroyForcibly();
throw new InterruptedException("任務(wù)被取消");
}
}
} catch (Exception e) {
// 忽略反射調(diào)用異常
}
}
// 非阻塞檢查進(jìn)程是否完成
if (process.isAlive()) {
// 進(jìn)程還在運(yùn)行,等待一小段時間后再檢查
Thread.sleep(1000);
} else {
// 進(jìn)程已完成,返回退出碼
return process.exitValue();
}
} catch (InterruptedException e) {
log.info("等待進(jìn)程時被中斷,正在強(qiáng)制終止進(jìn)程...");
process.destroyForcibly();
throw e;
}
}
}
/**
* 上傳切片文件目錄
*
* @param folder
* @return
*/
public static void uploadFolder(String folder) {
// 切片文件上傳至源文件同級目錄下的一個同名文件夾
File[] files = FileUtil.ls(folder);
if (files == null) {
return;
}
for (File file : files) {
uploadFile(file);
}
}
/**
* todo:自定義OSS上傳
*
* @param file
*/
private static void uploadFile(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
} catch (FileNotFoundException e) {
log.error("讀取文件報(bào)錯");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
原視頻文件:

切片后文件:

到此這篇關(guān)于SpringBoot基于FFmpeg實(shí)現(xiàn)壓縮視頻切片為m3u8的文章就介紹到這了,更多相關(guān)SpringBoot FFmpeg視頻壓縮內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析
這篇文章主要介紹了springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
關(guān)于JpaRepository的關(guān)聯(lián)查詢和@Query查詢
這篇文章主要介紹了JpaRepository的關(guān)聯(lián)查詢和@Query查詢,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Java中的Map接口實(shí)現(xiàn)類HashMap和LinkedHashMap詳解
這篇文章主要介紹了Java中的Map接口實(shí)現(xiàn)類HashMap和LinkedHashMap詳解,我們常會看到這樣的一種集合,IP地址與主機(jī)名,等,這種一一對應(yīng)的關(guān)系,就叫做映射,Java提供了專門的集合類用來存放這種對象關(guān)系的對象,需要的朋友可以參考下2024-01-01
JDK?version和class?file?version(Class編譯版本號)對應(yīng)關(guān)系解讀
這篇文章主要介紹了JDK?version和class?file?version(Class編譯版本號)對應(yīng)關(guān)系,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
springboot項(xiàng)目中常用的工具類和api詳解
在Spring Boot項(xiàng)目中,開發(fā)者通常會依賴一些工具類和API來簡化開發(fā)、提高效率,以下是一些常用的工具類及其典型應(yīng)用場景,涵蓋 Spring 原生工具、第三方庫(如Hutool、Guava) 和 Java 自帶工具,本文給大家介紹springboot項(xiàng)目中常用的工具類和api,感興趣的朋友一起看看吧2025-04-04
java學(xué)習(xí)筆記之eclipse+tomcat 配置
俗話說:工欲善其事必先利其器,既然要學(xué)習(xí)java,首先把java的開發(fā)環(huán)境搗鼓一下吧,這里我們來談?wù)別clipse+tomcat的配置方法。2014-11-11
zuul轉(zhuǎn)發(fā)后服務(wù)取不到請求路徑的解決
這篇文章主要介紹了zuul轉(zhuǎn)發(fā)后服務(wù)取不到請求路徑的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

