Java使用FFmpeg處理視頻文件的方法教程
前言
本文主要講述如何使用Java + FFmpeg實現(xiàn)對視頻文件的信息提取、碼率壓縮、分辨率轉(zhuǎn)換等功能;
之前在網(wǎng)上瀏覽了一大圈Java使用FFmpeg處理音視頻的文章,大多都講的比較簡單,樓主在實操過程中踩了很多坑也填了很多坑,希望這份詳細(xì)的踩坑&填坑指南能幫助到大家;
1. 什么是FFmpeg
2. 開發(fā)前準(zhǔn)備
在使用Java調(diào)用FFmpeg處理音視頻之前,需要先安裝FFmpeg,安裝方法分為兩種:
- 引入封裝了FFmpeg的開源框架
- 在系統(tǒng)中手動安裝FFmpeg
2.1 引入封裝了FFmpeg的開源框架
JAVE.jar(官網(wǎng)點我) 是一個封裝了FFmpeg的Java框架,在項目中能直接調(diào)用它的API來處理音視頻文件;
優(yōu)點:使用方便,直接在項目中引入JAVE.jar即可處理媒體文件,且開發(fā)完成后可以隨工程一起打包發(fā)布,不需要在目標(biāo)運行環(huán)境內(nèi)手動安裝FFmpeg相關(guān)的類庫
缺點:JAVE.jar最后一次更新是2009年,其封裝的FFmpeg版本是09年或更早前的版本,比較老舊,無法使用一些新特性
(當(dāng)然也可以看看有沒有其他比較新的封裝了FFmpeg的框架)
Maven坐標(biāo)如下:
<dependency> <groupId>org.ffmpeg</groupId> <artifactId>sdk</artifactId> <version>1.0.2</version> </dependency>
2.2 在系統(tǒng)中手動安裝FFmpeg
在運行環(huán)境中手動安裝FFmpeg稍微有一些麻煩,可以百度 windows/mac安裝FFmpeg 這樣的關(guān)鍵字,根據(jù)網(wǎng)上的安裝教程將FFmpeg安裝到系統(tǒng)中;
懶人鏈接:Windows安裝教程 Mac安裝教程
優(yōu)點:可以直接調(diào)用FFmpeg的相關(guān)API處理音視頻,F(xiàn)Fmpeg版本可控
缺點:手動安裝較為麻煩,開發(fā)環(huán)境與目標(biāo)運行環(huán)境都需要先安裝好FFmpeg
3. 使用FFmpeg處理音視頻
使用JAVE.jar進行開發(fā)與直接使用FFmpeg開發(fā)的代碼有一些不同,這里以直接使用FFmpeg進行開發(fā)的代碼進行講解(開發(fā)環(huán)境MacOS);(使用JAVE的代碼、直接使用FFmpeg的代碼都會附在文末供大家下載參考)
通過MediaUtil.java類及其依賴的類,你將可以實現(xiàn):
- 解析源視頻的基本信息,包括視頻格式、時長、碼率等;
- 解析音頻、圖片的基本信息;
- 將源視頻轉(zhuǎn)換成不同分辨率、不同碼率、帶或不帶音頻的新視頻;
- 抽取源視頻中指定時間點的幀畫面,來生成一張靜態(tài)圖;
- 抽取源視頻中指定時間段的幀畫面,來生成一個GIF動態(tài)圖;
- 截取源視頻中的一段來形成一個新視頻;
- 抽取源視頻中的音頻信息,生成單獨的MP3文件;
- 對音視頻等媒體文件執(zhí)行自定義的FFmpeg命令;
3.1 代碼結(jié)構(gòu)梳理
MediaUtil.java是整個解析程序中的核心類,封裝了各種常用的解析方法供外部調(diào)用;
MetaInfo.java定義了多媒體數(shù)據(jù)共有的一些屬性,VideoMetaInfo.java MusicMetaInfo.java ImageMetaInfo.java都繼承自MetaInfo.java,分別定義了視頻、音頻、圖片數(shù)據(jù)相關(guān)的一些屬性;
AnimatedGifEncoder.java LZWEncoder.java NeuQuant.java在抽取視頻幀數(shù)、制作GIF動態(tài)圖的時候會使用到;
CrfValueEnum.java 定義了三種常用的FFmpeg壓縮視頻時使用到的crf值,PresetVauleEnum.java定義了FFmpeg壓縮視頻時常用的幾種壓縮速率值;
3.2 MediaUtil.java主程序類解析
3.2.1 使用前需要注意的幾點
1、指定正確的FFmpeg程序執(zhí)行路徑
MacOS安裝好FFmpeg后,可以在控制臺中通過which ffmpeg命令獲取FFmpeg程序的執(zhí)行路徑,在調(diào)用MediaUtil.java前先通過其 setFFmpegPath() 方法設(shè)置好FFmpeg程序在系統(tǒng)中的執(zhí)行路徑,然后才能順利調(diào)用到FFmpeg去解析音視頻;
Windows系統(tǒng)下該路徑理論上應(yīng)設(shè)置為:FFmpeg可執(zhí)行程序在系統(tǒng)中的絕對路徑(實際情況有待大家補充)
2、指定解析音視頻信息時需要的正則表達(dá)式
因項目需要解析后綴格式為 .MP4 .WMV .AAC 的視頻和音頻文件,所以我研究了JAVE.jar底層調(diào)用FFmpeg時的解析邏輯后,在MediaUtil.java中設(shè)置好了匹配這三種格式的正則表達(dá)式供解析時使用(參考程序中的 durationRegex videoStreamRegex musicStreamRegex 這三個表達(dá)式值);
注意:如果你需要解析其他后綴格式如 .MKV .MP3 這樣的媒體文件時,你很可能需要根據(jù)實際情況修改durationRegex videoStreamRegex musicStreamRegex 這三個正則表達(dá)式的值,否則可能無法解析出正確的信息;
3、程序中的很多默認(rèn)值你可以根據(jù)實際需要修改,比如視頻幀抽取的默認(rèn)寬度或高度值、時長等等;
3.2.2 MediaUtil.java代碼
package media;
import lombok.extern.slf4j.Slf4j;
import media.domain.ImageMetaInfo;
import media.domain.MusicMetaInfo;
import media.domain.VideoMetaInfo;
import media.domain.gif.AnimatedGifEncoder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 基于FFmpeg內(nèi)核來編解碼音視頻信息;
* 使用前需手動在運行環(huán)境中安裝FFmpeg運行程序,然后正確設(shè)置FFmpeg運行路徑后MediaUtil.java才能正常調(diào)用到FFmpeg程序去處理音視頻;
*
* Author: dreamer-1
*
* version: 1.0
*
*/
@Slf4j
public class MediaUtil {
/**
* 可以處理的視頻格式
*/
public final static String[] VIDEO_TYPE = { "MP4", "WMV" };
/**
* 可以處理的圖片格式
*/
public final static String[] IMAGE_TYPE = { "JPG", "JPEG", "PNG", "GIF" };
/**
* 可以處理的音頻格式
*/
public final static String[] AUDIO_TYPE = { "AAC" };
/**
* 視頻幀抽取時的默認(rèn)時間點,第10s(秒)
* (Time類構(gòu)造參數(shù)的單位:ms)
*/
private static final Time DEFAULT_TIME = new Time(0, 0, 10);
/**
* 視頻幀抽取的默認(rèn)寬度值,單位:px
*/
private static int DEFAULT_WIDTH = 320;
/**
* 視頻幀抽取的默認(rèn)時長,單位:s(秒)
*/
private static int DEFAULT_TIME_LENGTH = 10;
/**
* 抽取多張視頻幀以合成gif動圖時,gif的播放速度
*/
private static int DEFAULT_GIF_PLAYTIME = 110;
/**
* FFmpeg程序執(zhí)行路徑
* 當(dāng)前系統(tǒng)安裝好ffmpeg程序并配置好相應(yīng)的環(huán)境變量后,值為ffmpeg可執(zhí)行程序文件在實際系統(tǒng)中的絕對路徑
*/
private static String FFMPEG_PATH = "/usr/bin/ffmpeg"; // /usr/bin/ffmpeg
/**
* 視頻時長正則匹配式
* 用于解析視頻及音頻的時長等信息時使用;
*
* (.*?)表示:匹配任何除\r\n之外的任何0或多個字符,非貪婪模式
*
*/
private static String durationRegex = "Duration: (\\d*?):(\\d*?):(\\d*?)\\.(\\d*?), start: (.*?), bitrate: (\\d*) kb\\/s.*";
private static Pattern durationPattern;
/**
* 視頻流信息正則匹配式
* 用于解析視頻詳細(xì)信息時使用;
*/
private static String videoStreamRegex = "Stream #\\d:\\d[\\(]??\\S*[\\)]??: Video: (\\S*\\S$?)[^\\,]*, (.*?), (\\d*)x(\\d*)[^\\,]*, (\\d*) kb\\/s, (\\d*[\\.]??\\d*) fps";
private static Pattern videoStreamPattern;
/**
* 音頻流信息正則匹配式
* 用于解析音頻詳細(xì)信息時使用;
*/
private static String musicStreamRegex = "Stream #\\d:\\d[\\(]??\\S*[\\)]??: Audio: (\\S*\\S$?)(.*), (.*?) Hz, (.*?), (.*?), (\\d*) kb\\/s";;
private static Pattern musicStreamPattern;
/**
* 靜態(tài)初始化時先加載好用于音視頻解析的正則匹配式
*/
static {
durationPattern = Pattern.compile(durationRegex);
videoStreamPattern = Pattern.compile(videoStreamRegex);
musicStreamPattern = Pattern.compile(musicStreamRegex);
}
/**
* 獲取當(dāng)前多媒體處理工具內(nèi)的ffmpeg的執(zhí)行路徑
* @return
*/
public static String getFFmpegPath() {
return FFMPEG_PATH;
}
/**
* 設(shè)置當(dāng)前多媒體工具內(nèi)的ffmpeg的執(zhí)行路徑
* @param ffmpeg_path ffmpeg可執(zhí)行程序在實際系統(tǒng)中的絕對路徑
* @return
*/
public static boolean setFFmpegPath(String ffmpeg_path) {
if (StringUtils.isBlank(ffmpeg_path)) {
log.error("--- 設(shè)置ffmpeg執(zhí)行路徑失敗,因為傳入的ffmpeg可執(zhí)行程序路徑為空! ---");
return false;
}
File ffmpegFile = new File(ffmpeg_path);
if (!ffmpegFile.exists()) {
log.error("--- 設(shè)置ffmpeg執(zhí)行路徑失敗,因為傳入的ffmpeg可執(zhí)行程序路徑下的ffmpeg文件不存在! ---");
return false;
}
FFMPEG_PATH = ffmpeg_path;
log.info("--- 設(shè)置ffmpeg執(zhí)行路徑成功 --- 當(dāng)前ffmpeg可執(zhí)行程序路徑為: " + ffmpeg_path);
return true;
}
/**
* 測試當(dāng)前多媒體工具是否可以正常工作
* @return
*/
public static boolean isExecutable() {
File ffmpegFile = new File(FFMPEG_PATH);
if (!ffmpegFile.exists()) {
log.error("--- 工作狀態(tài)異常,因為傳入的ffmpeg可執(zhí)行程序路徑下的ffmpeg文件不存在! ---");
return false;
}
List<String> cmds = new ArrayList<>(1);
cmds.add("-version");
String ffmpegVersionStr = executeCommand(cmds);
if (StringUtils.isBlank(ffmpegVersionStr)) {
log.error("--- 工作狀態(tài)異常,因為ffmpeg命令執(zhí)行失敗! ---");
return false;
}
log.info("--- 工作狀態(tài)正常 ---");
return true;
}
/**
* 執(zhí)行FFmpeg命令
* @param commonds 要執(zhí)行的FFmpeg命令
* @return FFmpeg程序在執(zhí)行命令過程中產(chǎn)生的各信息,執(zhí)行出錯時返回null
*/
public static String executeCommand(List<String> commonds) {
if (CollectionUtils.isEmpty(commonds)) {
log.error("--- 指令執(zhí)行失敗,因為要執(zhí)行的FFmpeg指令為空! ---");
return null;
}
LinkedList<String> ffmpegCmds = new LinkedList<>(commonds);
ffmpegCmds.addFirst(FFMPEG_PATH); // 設(shè)置ffmpeg程序所在路徑
log.info("--- 待執(zhí)行的FFmpeg指令為:---" + ffmpegCmds);
Runtime runtime = Runtime.getRuntime();
Process ffmpeg = null;
try {
// 執(zhí)行ffmpeg指令
ProcessBuilder builder = new ProcessBuilder();
builder.command(ffmpegCmds);
ffmpeg = builder.start();
log.info("--- 開始執(zhí)行FFmpeg指令:--- 執(zhí)行線程名:" + builder.toString());
// 取出輸出流和錯誤流的信息
// 注意:必須要取出ffmpeg在執(zhí)行命令過程中產(chǎn)生的輸出信息,如果不取的話當(dāng)輸出流信息填滿jvm存儲輸出留信息的緩沖區(qū)時,線程就回阻塞住
PrintStream errorStream = new PrintStream(ffmpeg.getErrorStream());
PrintStream inputStream = new PrintStream(ffmpeg.getInputStream());
errorStream.start();
inputStream.start();
// 等待ffmpeg命令執(zhí)行完
ffmpeg.waitFor();
// 獲取執(zhí)行結(jié)果字符串
String result = errorStream.stringBuffer.append(inputStream.stringBuffer).toString();
// 輸出執(zhí)行的命令信息
String cmdStr = Arrays.toString(ffmpegCmds.toArray()).replace(",", "");
String resultStr = StringUtils.isBlank(result) ? "【異?!? : "正常";
log.info("--- 已執(zhí)行的FFmepg命令: ---" + cmdStr + " 已執(zhí)行完畢,執(zhí)行結(jié)果: " + resultStr);
return result;
} catch (Exception e) {
log.error("--- FFmpeg命令執(zhí)行出錯! --- 出錯信息: " + e.getMessage());
return null;
} finally {
if (null != ffmpeg) {
ProcessKiller ffmpegKiller = new ProcessKiller(ffmpeg);
// JVM退出時,先通過鉤子關(guān)閉FFmepg進程
runtime.addShutdownHook(ffmpegKiller);
}
}
}
/**
* 視頻轉(zhuǎn)換
*
* 注意指定視頻分辨率時,寬度和高度必須同時有值;
*
* @param fileInput 源視頻路徑
* @param fileOutPut 轉(zhuǎn)換后的視頻輸出路徑
* @param withAudio 是否保留音頻;true-保留,false-不保留
* @param crf 指定視頻的質(zhì)量系數(shù)(值越小,視頻質(zhì)量越高,體積越大;該系數(shù)取值為0-51,直接影響視頻碼率大?。?取值參考:CrfValueEnum.code
* @param preset 指定視頻的編碼速率(速率越快壓縮率越低),取值參考:PresetVauleEnum.presetValue
* @param width 視頻寬度;為空則保持源視頻寬度
* @param height 視頻高度;為空則保持源視頻高度
*/
public static void convertVideo(File fileInput, File fileOutPut, boolean withAudio, Integer crf, String preset, Integer width, Integer height) {
if (null == fileInput || !fileInput.exists()) {
throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑");
}
if (null == fileOutPut) {
throw new RuntimeException("轉(zhuǎn)換后的視頻路徑為空,請檢查轉(zhuǎn)換后的視頻存放路徑是否正確");
}
if (!fileOutPut.exists()) {
try {
fileOutPut.createNewFile();
} catch (IOException e) {
log.error("視頻轉(zhuǎn)換時新建輸出文件失敗");
}
}
String format = getFormat(fileInput);
if (!isLegalFormat(format, VIDEO_TYPE)) {
throw new RuntimeException("無法解析的視頻格式:" + format);
}
List<String> commond = new ArrayList<String>();
commond.add("-i");
commond.add(fileInput.getAbsolutePath());
if (!withAudio) { // 設(shè)置是否保留音頻
commond.add("-an"); // 去掉音頻
}
if (null != width && width > 0 && null != height && height > 0) { // 設(shè)置分辨率
commond.add("-s");
String resolution = width.toString() + "x" + height.toString();
commond.add(resolution);
}
commond.add("-vcodec"); // 指定輸出視頻文件時使用的編碼器
commond.add("libx264"); // 指定使用x264編碼器
commond.add("-preset"); // 當(dāng)使用x264時需要帶上該參數(shù)
commond.add(preset); // 指定preset參數(shù)
commond.add("-crf"); // 指定輸出視頻質(zhì)量
commond.add(crf.toString()); // 視頻質(zhì)量參數(shù),值越小視頻質(zhì)量越高
commond.add("-y"); // 當(dāng)已存在輸出文件時,不提示是否覆蓋
commond.add(fileOutPut.getAbsolutePath());
executeCommand(commond);
}
/**
* 視頻幀抽取
* 默認(rèn)抽取第10秒的幀畫面
* 抽取的幀圖片默認(rèn)寬度為300px
*
* 轉(zhuǎn)換后的文件路徑以.gif結(jié)尾時,默認(rèn)截取從第10s開始,后10s以內(nèi)的幀畫面來生成gif
*
* @param videoFile 源視頻路徑
* @param fileOutPut 轉(zhuǎn)換后的文件路徑
*/
public static void cutVideoFrame(File videoFile, File fileOutPut) {
cutVideoFrame(videoFile, fileOutPut, DEFAULT_TIME);
}
/**
* 視頻幀抽?。ǔ槿≈付〞r間點的幀畫面)
* 抽取的視頻幀圖片寬度默認(rèn)為320px
*
* 轉(zhuǎn)換后的文件路徑以.gif結(jié)尾時,默認(rèn)截取從指定時間點開始,后10s以內(nèi)的幀畫面來生成gif
*
* @param videoFile 源視頻路徑
* @param fileOutPut 轉(zhuǎn)換后的文件路徑
* @param time 指定抽取視頻幀的時間點(單位:s)
*/
public static void cutVideoFrame(File videoFile, File fileOutPut, Time time) {
cutVideoFrame(videoFile, fileOutPut, time, DEFAULT_WIDTH);
}
/**
* 視頻幀抽?。ǔ槿≈付〞r間點、指定寬度值的幀畫面)
* 只需指定視頻幀的寬度,高度隨寬度自動計算
*
* 轉(zhuǎn)換后的文件路徑以.gif結(jié)尾時,默認(rèn)截取從指定時間點開始,后10s以內(nèi)的幀畫面來生成gif
*
* @param videoFile 源視頻路徑
* @param fileOutPut 轉(zhuǎn)換后的文件路徑
* @param time 指定要抽取第幾秒的視頻幀(單位:s)
* @param width 抽取的視頻幀圖片的寬度(單位:px)
*/
public static void cutVideoFrame(File videoFile, File fileOutPut, Time time, int width) {
if (null == videoFile || !videoFile.exists()) {
throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑");
}
if (null == fileOutPut) {
throw new RuntimeException("轉(zhuǎn)換后的視頻路徑為空,請檢查轉(zhuǎn)換后的視頻存放路徑是否正確");
}
VideoMetaInfo info = getVideoMetaInfo(videoFile);
if (null == info) {
log.error("--- 未能解析源視頻信息,視頻幀抽取操作失敗 --- 源視頻: " + videoFile);
return;
}
int height = width * info.getHeight() / info.getWidth(); // 根據(jù)寬度計算適合的高度,防止畫面變形
cutVideoFrame(videoFile, fileOutPut, time, width, height);
}
/**
* 視頻幀抽?。ǔ槿≈付〞r間點、指定寬度值、指定高度值的幀畫面)
*
* 轉(zhuǎn)換后的文件路徑以.gif結(jié)尾時,默認(rèn)截取從指定時間點開始,后10s以內(nèi)的幀畫面來生成gif
*
* @param videoFile 源視頻路徑
* @param fileOutPut 轉(zhuǎn)換后的文件路徑
* @param time 指定要抽取第幾秒的視頻幀(單位:s)
* @param width 抽取的視頻幀圖片的寬度(單位:px)
* @param height 抽取的視頻幀圖片的高度(單位:px)
*/
public static void cutVideoFrame(File videoFile, File fileOutPut, Time time, int width, int height) {
if (null == videoFile || !videoFile.exists()) {
throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑");
}
if (null == fileOutPut) {
throw new RuntimeException("轉(zhuǎn)換后的視頻路徑為空,請檢查轉(zhuǎn)換后的視頻存放路徑是否正確");
}
String format = getFormat(fileOutPut);
if (!isLegalFormat(format, IMAGE_TYPE)) {
throw new RuntimeException("無法生成指定格式的幀圖片:" + format);
}
String fileOutPutPath = fileOutPut.getAbsolutePath();
if (!"GIF".equals(StringUtils.upperCase(format))) {
// 輸出路徑不是以.gif結(jié)尾,抽取并生成一張靜態(tài)圖
cutVideoFrame(videoFile, fileOutPutPath, time, width, height, 1, false);
} else {
// 抽取并生成一個gif(gif由10張靜態(tài)圖構(gòu)成)
String path = fileOutPut.getParent();
String name = fileOutPut.getName();
// 創(chuàng)建臨時文件存儲多張靜態(tài)圖用于生成gif
String tempPath = path + File.separator + System.currentTimeMillis() + "_" + name.substring(0, name.indexOf("."));
File file = new File(tempPath);
if (!file.exists()) {
file.mkdir();
}
try {
cutVideoFrame(videoFile, tempPath, time, width, height, DEFAULT_TIME_LENGTH, true);
// 生成gif
String images[] = file.list();
for (int i = 0; i < images.length; i++) {
images[i] = tempPath + File.separator + images[i];
}
createGifImage(images, fileOutPut.getAbsolutePath(), DEFAULT_GIF_PLAYTIME);
} catch (Exception e) {
log.error("--- 截取視頻幀操作出錯 --- 錯誤信息:" + e.getMessage());
} finally {
// 刪除用于生成gif的臨時文件
String images[] = file.list();
for (int i = 0; i < images.length; i++) {
File fileDelete = new File(tempPath + File.separator + images[i]);
fileDelete.delete();
}
file.delete();
}
}
}
/**
* 視頻幀抽?。ǔ槿≈付〞r間點、指定寬度值、指定高度值、指定時長、指定單張/多張的幀畫面)
*
* @param videoFile 源視頻
* @param path 轉(zhuǎn)換后的文件輸出路徑
* @param time 開始截取視頻幀的時間點(單位:s)
* @param width 截取的視頻幀圖片的寬度(單位:px)
* @param height 截取的視頻幀圖片的高度(單位:px,需要大于20)
* @param timeLength 截取的視頻幀的時長(從time開始算,單位:s,需小于源視頻的最大時長)
* @param isContinuty false - 靜態(tài)圖(只截取time時間點的那一幀圖片),true - 動態(tài)圖(截取從time時間點開始,timelength這段時間內(nèi)的多張幀圖)
*/
private static void cutVideoFrame(File videoFile, String path, Time time, int width, int height, int timeLength, boolean isContinuty) {
if (videoFile == null || !videoFile.exists()) {
throw new RuntimeException("源視頻文件不存在,源視頻路徑: ");
}
if (null == path) {
throw new RuntimeException("轉(zhuǎn)換后的文件路徑為空,請檢查轉(zhuǎn)換后的文件存放路徑是否正確");
}
VideoMetaInfo info = getVideoMetaInfo(videoFile);
if (null == info) {
throw new RuntimeException("未解析到視頻信息");
}
if (time.getTime() + timeLength > info.getDuration()) {
throw new RuntimeException("開始截取視頻幀的時間點不合法:" + time.toString() + ",因為截取時間點晚于視頻的最后時間點");
}
if (width <= 20 || height <= 20) {
throw new RuntimeException("截取的視頻幀圖片的寬度或高度不合法,寬高值必須大于20");
}
try {
List<String> commond = new ArrayList<String>();
commond.add("-ss");
commond.add(time.toString());
if (isContinuty) {
commond.add("-t");
commond.add(timeLength + "");
} else {
commond.add("-vframes");
commond.add("1");
}
commond.add("-i");
commond.add(videoFile.getAbsolutePath());
commond.add("-an");
commond.add("-f");
commond.add("image2");
if (isContinuty) {
commond.add("-r");
commond.add("3");
}
commond.add("-s");
commond.add(width + "*" + height);
if (isContinuty) {
commond.add(path + File.separator + "foo-%03d.jpeg");
} else {
commond.add(path);
}
executeCommand(commond);
} catch (Exception e) {
log.error("--- 視頻幀抽取過程出錯 --- 錯誤信息: " + e.getMessage());
}
}
/**
* 截取視頻中的某一段,生成新視頻
*
* @param videoFile 源視頻路徑
* @param outputFile 轉(zhuǎn)換后的視頻路徑
* @param startTime 開始抽取的時間點(單位:s)
* @param timeLength 需要抽取的時間段(單位:s,需小于源視頻最大時長);例如:該參數(shù)值為10時即抽取從startTime開始之后10秒內(nèi)的視頻作為新視頻
*/
public static void cutVideo(File videoFile, File outputFile, Time startTime, int timeLength) {
if (videoFile == null || !videoFile.exists()) {
throw new RuntimeException("視頻文件不存在:");
}
if (null == outputFile) {
throw new RuntimeException("轉(zhuǎn)換后的視頻路徑為空,請檢查轉(zhuǎn)換后的視頻存放路徑是否正確");
}
VideoMetaInfo info = getVideoMetaInfo(videoFile);
if (null == info) {
throw new RuntimeException("未解析到視頻信息");
}
if (startTime.getTime() + timeLength > info.getDuration()) {
throw new RuntimeException("截取時間不合法:" + startTime.toString() + ",因為截取時間大于視頻的時長");
}
try {
if (!outputFile.exists()) {
outputFile.createNewFile();
}
List<String> commond = new ArrayList<String>();
commond.add("-ss");
commond.add(startTime.toString());
commond.add("-t");
commond.add("" + timeLength);
commond.add("-i");
commond.add(videoFile.getAbsolutePath());
commond.add("-vcodec");
commond.add("copy");
commond.add("-acodec");
commond.add("copy");
commond.add(outputFile.getAbsolutePath());
executeCommand(commond);
} catch (IOException e) {
log.error("--- 視頻截取過程出錯 ---");
}
}
/**
* 抽取視頻里的音頻信息
* 只能抽取成MP3文件
* @param videoFile 源視頻文件
* @param audioFile 從源視頻提取的音頻文件
*/
public static void getAudioFromVideo(File videoFile, File audioFile) {
if (null == videoFile || !videoFile.exists()) {
throw new RuntimeException("源視頻文件不存在: ");
}
if (null == audioFile) {
throw new RuntimeException("要提取的音頻路徑為空:");
}
String format = getFormat(audioFile);
if (!isLegalFormat(format, AUDIO_TYPE)) {
throw new RuntimeException("無法生成指定格式的音頻:" + format + " 請檢查要輸出的音頻文件是否是AAC類型");
}
try {
if (!audioFile.exists()) {
audioFile.createNewFile();
}
List<String> commond = new ArrayList<String>();
commond.add("-i");
commond.add(videoFile.getAbsolutePath());
commond.add("-vn"); // no video,去除視頻信息
commond.add("-y");
commond.add("-acodec");
commond.add("copy");
commond.add(audioFile.getAbsolutePath());
executeCommand(commond);
} catch (Exception e) {
log.error("--- 抽取視頻中的音頻信息的過程出錯 --- 錯誤信息: " + e.getMessage());
}
}
/**
* 解析視頻的基本信息(從文件中)
*
* 解析出的視頻信息一般為以下格式:
* Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '6.mp4':
* Duration: 00:00:30.04, start: 0.000000, bitrate: 19031 kb/s
* Stream #0:0(eng): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080, 18684 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default)
* Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 317 kb/s (default)
*
* 注解:
* Duration: 00:00:30.04【視頻時長】, start: 0.000000【視頻開始時間】, bitrate: 19031 kb/s【視頻比特率/碼率】
* Stream #0:0(eng): Video: h264【視頻編碼格式】 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080【視頻分辨率,寬x高】, 18684【視頻比特率】 kb/s, 25【視頻幀率】 fps, 25 tbr, 25k tbn, 50 tbc (default)
* Stream #0:1(eng): Audio: aac【音頻格式】 (LC) (mp4a / 0x6134706D), 48000【音頻采樣率】 Hz, stereo, fltp, 317【音頻碼率】 kb/s (default)
*
* @param videoFile 源視頻路徑
* @return 視頻的基本信息,解碼失敗時返回null
*/
public static VideoMetaInfo getVideoMetaInfo(File videoFile) {
if (null == videoFile || !videoFile.exists()) {
log.error("--- 解析視頻信息失敗,因為要解析的源視頻文件不存在 ---");
return null;
}
VideoMetaInfo videoInfo = new VideoMetaInfo();
String parseResult = getMetaInfoFromFFmpeg(videoFile);
Matcher durationMacher = durationPattern.matcher(parseResult);
Matcher videoStreamMacher = videoStreamPattern.matcher(parseResult);
Matcher videoMusicStreamMacher = musicStreamPattern.matcher(parseResult);
Long duration = 0L; // 視頻時長
Integer videoBitrate = 0; // 視頻碼率
String videoFormat = getFormat(videoFile); // 視頻格式
Long videoSize = videoFile.length(); // 視頻大小
String videoEncoder = ""; // 視頻編碼器
Integer videoHeight = 0; // 視頻高度
Integer videoWidth = 0; // 視頻寬度
Float videoFramerate = 0F; // 視頻幀率
String musicFormat = ""; // 音頻格式
Long samplerate = 0L; // 音頻采樣率
Integer musicBitrate = 0; // 音頻碼率
try {
// 匹配視頻播放時長等信息
if (durationMacher.find()) {
long hours = (long)Integer.parseInt(durationMacher.group(1));
long minutes = (long)Integer.parseInt(durationMacher.group(2));
long seconds = (long)Integer.parseInt(durationMacher.group(3));
long dec = (long)Integer.parseInt(durationMacher.group(4));
duration = dec * 100L + seconds * 1000L + minutes * 60L * 1000L + hours * 60L * 60L * 1000L;
//String startTime = durationMacher.group(5) + "ms";
videoBitrate = Integer.parseInt(durationMacher.group(6));
}
// 匹配視頻分辨率等信息
if (videoStreamMacher.find()) {
videoEncoder = videoStreamMacher.group(1);
String s2 = videoStreamMacher.group(2);
videoWidth = Integer.parseInt(videoStreamMacher.group(3));
videoHeight = Integer.parseInt(videoStreamMacher.group(4));
String s5 = videoStreamMacher.group(5);
videoFramerate = Float.parseFloat(videoStreamMacher.group(6));
}
// 匹配視頻中的音頻信息
if (videoMusicStreamMacher.find()) {
musicFormat = videoMusicStreamMacher.group(1); // 提取音頻格式
//String s2 = videoMusicStreamMacher.group(2);
samplerate = Long.parseLong(videoMusicStreamMacher.group(3)); // 提取采樣率
//String s4 = videoMusicStreamMacher.group(4);
//String s5 = videoMusicStreamMacher.group(5);
musicBitrate = Integer.parseInt(videoMusicStreamMacher.group(6)); // 提取比特率
}
} catch (Exception e) {
log.error("--- 解析視頻參數(shù)信息出錯! --- 錯誤信息: " + e.getMessage());
return null;
}
// 封裝視頻中的音頻信息
MusicMetaInfo musicMetaInfo = new MusicMetaInfo();
musicMetaInfo.setFormat(musicFormat);
musicMetaInfo.setDuration(duration);
musicMetaInfo.setBitRate(musicBitrate);
musicMetaInfo.setSampleRate(samplerate);
// 封裝視頻信息
VideoMetaInfo videoMetaInfo = new VideoMetaInfo();
videoMetaInfo.setFormat(videoFormat);
videoMetaInfo.setSize(videoSize);
videoMetaInfo.setBitRate(videoBitrate);
videoMetaInfo.setDuration(duration);
videoMetaInfo.setEncoder(videoEncoder);
videoMetaInfo.setFrameRate(videoFramerate);
videoMetaInfo.setHeight(videoHeight);
videoMetaInfo.setWidth(videoWidth);
videoMetaInfo.setMusicMetaInfo(musicMetaInfo);
return videoMetaInfo;
}
/**
* 獲取視頻的基本信息(從流中)
*
* @param inputStream 源視頻流路徑
* @return 視頻的基本信息,解碼失敗時返回null
*/
public static VideoMetaInfo getVideoMetaInfo(InputStream inputStream) {
VideoMetaInfo videoInfo = new VideoMetaInfo();
try {
File file = File.createTempFile("tmp", null);
if (!file.exists()) {
return null;
}
FileUtils.copyInputStreamToFile(inputStream, file);
videoInfo = getVideoMetaInfo(file);
file.deleteOnExit();
return videoInfo;
} catch (Exception e) {
log.error("--- 從流中獲取視頻基本信息出錯 --- 錯誤信息: " + e.getMessage());
return null;
}
}
/**
* 獲取音頻的基本信息(從文件中)
* @param musicFile 音頻文件路徑
* @return 音頻的基本信息,解碼失敗時返回null
*/
public static MusicMetaInfo getMusicMetaInfo(File musicFile) {
if (null == musicFile || !musicFile.exists()) {
log.error("--- 無法獲取音頻信息,因為要解析的音頻文件為空 ---");
return null;
}
// 獲取音頻信息字符串,方便后續(xù)解析
String parseResult = getMetaInfoFromFFmpeg(musicFile);
Long duration = 0L; // 音頻時長
Integer musicBitrate = 0; // 音頻碼率
Long samplerate = 0L; // 音頻采樣率
String musicFormat = ""; // 音頻格式
Long musicSize = musicFile.length(); // 音頻大小
Matcher durationMacher = durationPattern.matcher(parseResult);
Matcher musicStreamMacher = musicStreamPattern.matcher(parseResult);
try {
// 匹配音頻播放時長等信息
if (durationMacher.find()) {
long hours = (long)Integer.parseInt(durationMacher.group(1));
long minutes = (long)Integer.parseInt(durationMacher.group(2));
long seconds = (long)Integer.parseInt(durationMacher.group(3));
long dec = (long)Integer.parseInt(durationMacher.group(4));
duration = dec * 100L + seconds * 1000L + minutes * 60L * 1000L + hours * 60L * 60L * 1000L;
//String startTime = durationMacher.group(5) + "ms";
musicBitrate = Integer.parseInt(durationMacher.group(6));
}
// 匹配音頻采樣率等信息
if (musicStreamMacher.find()) {
musicFormat = musicStreamMacher.group(1); // 提取音頻格式
//String s2 = videoMusicStreamMacher.group(2);
samplerate = Long.parseLong(musicStreamMacher.group(3)); // 提取采樣率
//String s4 = videoMusicStreamMacher.group(4);
//String s5 = videoMusicStreamMacher.group(5);
musicBitrate = Integer.parseInt(musicStreamMacher.group(6)); // 提取比特率
}
} catch (Exception e) {
log.error("--- 解析音頻參數(shù)信息出錯! --- 錯誤信息: " + e.getMessage());
return null;
}
// 封裝視頻中的音頻信息
MusicMetaInfo musicMetaInfo = new MusicMetaInfo();
musicMetaInfo.setFormat(musicFormat);
musicMetaInfo.setDuration(duration);
musicMetaInfo.setBitRate(musicBitrate);
musicMetaInfo.setSampleRate(samplerate);
musicMetaInfo.setSize(musicSize);
return musicMetaInfo;
}
/**
* 獲取音頻的基本信息(從流中)
* @param inputStream 源音樂流路徑
* @return 音頻基本信息,解碼出錯時返回null
*/
public static MusicMetaInfo getMusicMetaInfo(InputStream inputStream) {
MusicMetaInfo musicMetaInfo = new MusicMetaInfo();
try {
File file = File.createTempFile("tmp", null);
if (!file.exists()) {
return null;
}
FileUtils.copyInputStreamToFile(inputStream, file);
musicMetaInfo = getMusicMetaInfo(file);
file.deleteOnExit();
return musicMetaInfo;
} catch (Exception e) {
log.error("--- 從流中獲取音頻基本信息出錯 --- 錯誤信息: " + e.getMessage());
return null;
}
}
/**
* 獲取圖片的基本信息(從流中)
*
* @param inputStream 源圖片路徑
* @return 圖片的基本信息,獲取信息失敗時返回null
*/
public static ImageMetaInfo getImageInfo(InputStream inputStream) {
BufferedImage image = null;
ImageMetaInfo imageInfo = new ImageMetaInfo();
try {
image = ImageIO.read(inputStream);
imageInfo.setWidth(image.getWidth());
imageInfo.setHeight(image.getHeight());
imageInfo.setSize(Long.valueOf(String.valueOf(inputStream.available())));
return imageInfo;
} catch (Exception e) {
log.error("--- 獲取圖片的基本信息失敗 --- 錯誤信息: " + e.getMessage());
return null;
}
}
/**
* 獲取圖片的基本信息 (從文件中)
*
* @param imageFile 源圖片路徑
* @return 圖片的基本信息,獲取信息失敗時返回null
*/
public static ImageMetaInfo getImageInfo(File imageFile) {
BufferedImage image = null;
ImageMetaInfo imageInfo = new ImageMetaInfo();
try {
if (null == imageFile || !imageFile.exists()) {
return null;
}
image = ImageIO.read(imageFile);
imageInfo.setWidth(image.getWidth());
imageInfo.setHeight(image.getHeight());
imageInfo.setSize(imageFile.length());
imageInfo.setFormat(getFormat(imageFile));
return imageInfo;
} catch (Exception e) {
log.error("--- 獲取圖片的基本信息失敗 --- 錯誤信息: " + e.getMessage());
return null;
}
}
/**
* 檢查文件類型是否是給定的類型
* @param inputFile 源文件
* @param givenFormat 指定的文件類型;例如:{"MP4", "AVI"}
* @return
*/
public static boolean isGivenFormat(File inputFile, String[] givenFormat) {
if (null == inputFile || !inputFile.exists()) {
log.error("--- 無法檢查文件類型是否滿足要求,因為要檢查的文件不存在 --- 源文件: " + inputFile);
return false;
}
if (null == givenFormat || givenFormat.length <= 0) {
log.error("--- 無法檢查文件類型是否滿足要求,因為沒有指定的文件類型 ---");
return false;
}
String fomat = getFormat(inputFile);
return isLegalFormat(fomat, givenFormat);
}
/**
* 使用FFmpeg的"-i"命令來解析視頻信息
* @param inputFile 源媒體文件
* @return 解析后的結(jié)果字符串,解析失敗時為空
*/
public static String getMetaInfoFromFFmpeg(File inputFile) {
if (inputFile == null || !inputFile.exists()) {
throw new RuntimeException("源媒體文件不存在,源媒體文件路徑: ");
}
List<String> commond = new ArrayList<String>();
commond.add("-i");
commond.add(inputFile.getAbsolutePath());
String executeResult = MediaUtil.executeCommand(commond);
return executeResult;
}
/**
* 檢測視頻格式是否合法
* @param format
* @param formats
* @return
*/
private static boolean isLegalFormat(String format, String formats[]) {
for (String item : formats) {
if (item.equals(StringUtils.upperCase(format))) {
return true;
}
}
return false;
}
/**
* 創(chuàng)建gif
*
* @param image 多個jpg文件名(包含路徑)
* @param outputPath 生成的gif文件名(包含路徑)
* @param playTime 播放的延遲時間,可調(diào)整gif的播放速度
*/
private static void createGifImage(String image[], String outputPath, int playTime) {
if (null == outputPath) {
throw new RuntimeException("轉(zhuǎn)換后的GIF路徑為空,請檢查轉(zhuǎn)換后的GIF存放路徑是否正確");
}
try {
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
encoder.setRepeat(0);
encoder.start(outputPath);
BufferedImage src[] = new BufferedImage[image.length];
for (int i = 0; i < src.length; i++) {
encoder.setDelay(playTime); // 設(shè)置播放的延遲時間
src[i] = ImageIO.read(new File(image[i])); // 讀入需要播放的jpg文件
encoder.addFrame(src[i]); // 添加到幀中
}
encoder.finish();
} catch (Exception e) {
log.error("--- 多張靜態(tài)圖轉(zhuǎn)換成動態(tài)GIF圖的過程出錯 --- 錯誤信息: " + e.getMessage());
}
}
/**
* 獲取指定文件的后綴名
* @param file
* @return
*/
private static String getFormat(File file) {
String fileName = file.getName();
String format = fileName.substring(fileName.indexOf(".") + 1);
return format;
}
/**
* 在程序退出前結(jié)束已有的FFmpeg進程
*/
private static class ProcessKiller extends Thread {
private Process process;
public ProcessKiller(Process process) {
this.process = process;
}
@Override
public void run() {
this.process.destroy();
log.info("--- 已銷毀FFmpeg進程 --- 進程名: " + process.toString());
}
}
/**
* 用于取出ffmpeg線程執(zhí)行過程中產(chǎn)生的各種輸出和錯誤流的信息
*/
static class PrintStream extends Thread {
InputStream inputStream = null;
BufferedReader bufferedReader = null;
StringBuffer stringBuffer = new StringBuffer();
public PrintStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
try {
if (null == inputStream) {
log.error("--- 讀取輸出流出錯!因為當(dāng)前輸出流為空!---");
}
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
log.info(line);
stringBuffer.append(line);
}
} catch (Exception e) {
log.error("--- 讀取輸入流出錯了!--- 錯誤信息:" + e.getMessage());
} finally {
try {
if (null != bufferedReader) {
bufferedReader.close();
}
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
log.error("--- 調(diào)用PrintStream讀取輸出流后,關(guān)閉流時出錯!---");
}
}
}
}
}
3.2.3 踩坑&填坑
1、在Linux等服務(wù)器上部署Java程序進行視頻壓縮時,多注意一下運行賬號的權(quán)限問題,有時候可能是由于運行程序沒有足夠的文件操作權(quán)限,導(dǎo)致壓縮過程失敗;
2、第一版程序上線后,偶爾會出現(xiàn)這樣的問題:
調(diào)用MediaUtil.java進行視頻壓縮過程中,整個程序突然“卡住”,后臺也沒有日志再打印出來,此時整個壓縮過程還沒有完成,像是線程突然阻塞住了;
經(jīng)過多番查找,發(fā)現(xiàn)Java調(diào)用FFmpeg時,實際是在JVM里產(chǎn)生一個子進程來執(zhí)行壓縮過程,這個子進程與JVM建立三個通道鏈接(包括標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤流),在壓縮過程中,實際會不停地向標(biāo)準(zhǔn)輸出和錯誤流中寫入信息;
因為本地系統(tǒng)對標(biāo)準(zhǔn)輸出及錯誤流提供的緩沖區(qū)大小有限,當(dāng)寫入標(biāo)準(zhǔn)輸出和錯誤流的信息填滿緩沖區(qū)時,執(zhí)行壓縮的進程就會阻塞住;
所以在壓縮過程中,需要單獨創(chuàng)建兩個線程不停讀取標(biāo)準(zhǔn)輸出及錯誤流中的信息,防止整個壓縮進程阻塞;(參考MediaUtil.java中的 executeCommand() 方法中的 errorStream 和 inputStream 這兩個內(nèi)部類實例的操作)
3.3 在CentOS服務(wù)器安裝FFmpeg指南
因項目最后部署在CentOS服務(wù)器上,需提前在服務(wù)器上安裝好FFmpeg程序,這過程中也踩了不少坑,針對此寫了另一篇總結(jié)文章,參考這里 點我哦
4. 源碼下載
這里提供兩種版本的源碼供大家下載參考:
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
一文教會Java新手使用Spring?MVC中的查詢字符串和查詢參數(shù)
在使用springMVC框架構(gòu)建web應(yīng)用,客戶端常會請求字符串、整型、json等格式的數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于通過一文教會Java新手使用Spring?MVC中的查詢字符串和查詢參數(shù)的相關(guān)資料,需要的朋友可以參考下2024-01-01
Netty分布式pipeline管道創(chuàng)建方法跟蹤解析
這篇文章主要為大家介紹了Netty分布式pipeline管道創(chuàng)建方法跟蹤解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-03-03
Spring?boot框架JWT實現(xiàn)用戶賬戶密碼登錄驗證流程
這篇文章主要介紹了Springboot框架JWT實現(xiàn)用戶賬戶密碼登錄驗證,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06
java?Comparable和Comparator的區(qū)別及作用面試精講
這篇文章主要為大家介紹了java?Comparable和Comparator的區(qū)別及作用面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10
Springboot實現(xiàn)多線程注入bean的工具類操作
這篇文章主要介紹了Springboot實現(xiàn)多線程注入bean的工具類操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08

