此文已由作者葉海嘯授權網易雲社區發布。
歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。
FFmpeg是一個開源免費跨平台的視頻和音頻流方案,可以快速對音視頻流進行多方面的處理,本文主要介紹FFmpeg常用的命令與參數講解,如何在JAVA中使用FFmpeg以及遇到的一些問題。
背景
項目需求中涉及到有關於視頻、音頻的一系列處理,包含視頻中音頻提取、視頻首幀提取、音頻重采樣、字幕壓縮的功能,一直在研究ffmpeg,僅僅幾個功能,卻深受ffmpeg的折磨。
今天談談ffmpeg在java中的簡單使用,首先下載ffmpeg包,官方地址:http://ffmpeg.org/download.html, 這里建議下載Linux Static Builds版本的,輕小而且解壓后可以直接使用,本人使用的版本是ffmpeg-git-20170922-64bit-static.tar.xz。
解壓之后,文件夾中有一個可執行文件ffmpeg,在linux上可以直接運行./ffmpeg -version,可以查看ffmpeg的版本信息,以及configuration配置信息。
基本命令
視頻中音頻提取:ffmpeg -i [videofile] -vn -acodec copy [targetaudiofile]
視頻首幀提取:ffmpeg -i [videofile] -vframes 1 -q:v 2 -f image2 [imagefile]
音頻重采樣:ffmpeg -i [audiofile] -ar [samplingrate] [targetaudiofile]
字幕壓縮:ffmpeg -i [videofile] -vf subtitles=[subtitle.srt] [targetvideofile]
命令說明
audiofile、videofile是音視頻源文件,可以是本地文件,也可以是網絡文件URL;
提取音頻流時,-vn 忽略視頻流 -acodec 設定聲音編解碼器,未設定時則使用與輸入流相同的編解碼器,如果需要提取視頻流,則參數變為-an -vcodec;
-vframes 表示提取的第幾幀,獲取第一楨則后面的值為1,如果后面的值大於1,那么最后的[imagefile]不能指定一個文件,不然會報錯,如下
指定了輸出的文件名為“1.jpeg”,報錯:不能從1.jpeg文件中獲取第二幀的文件名,因為-vframes只要大於1,則會提取出每一幀的圖片,建議使用如%03d.jpeg來作為文件名,那么它解析的結果便是001.jpeg,002.jpeg,...依次編號往后;
-q:v 2 q代表質量quality, v代表視頻流,2是控制質量的參數;-f指定輸出的格式是image2;
除了使用-vframes來獲取視頻幀,還有使用-ss參數來獲取,-ss后的時間參數是自行設定,並且在視頻的有效時間內(格式為00:00:00),使用-ss時,如果沒有使用%03d.jpeg來作為文件名,則獲取的是-ss參數指定的那個時間的幀;
-ar表示使用新的采樣率,常用的有8,000Hz、16,000Hz、22,050Hz、32,000Hz、44,100Hz;
subtitle.srt是字幕文件(中文字幕即把英文變為中文,其它格式一致),這邊就使用最簡單的srt標准格式,srt文件寫入的字符編碼需要是UTF-8,否則壓縮的時候會報無法讀取srt文件;
若想壓縮中文字幕,需要系統中有中文字體,使用fc-list查詢系統支持的字體,fc-list :lang=zh查詢支持的中文字體
JAVA使用遇到的問題
一般需要調用系統命令時,大部分人第一反應肯定是使用Runtime.getRuntime().exec(command)返回一個process對象,再調用process.waitFor()來等待命令執行結束,獲取執行結果。
產品剛上線,運行很穩定,但是沒過多久,產品同學說從某個時間點開始添加的視頻都不出來了!因為這個視頻必須要經過一系列的處理,才會展示出來,所以中途某個環節出錯了。
首先查看了日志,沒有發現任何的報錯,但是幸好開發的時候加了debug日志,每一句命令exec前后都會打一句log,於是看下是否“開始執行”和“執行成功”兩句log都打印了,結果發現在截取首幀的時候,只打印了“開始執行”,一直沒有結束,那么猜測進程堵塞了。
但是,我把產品同學的視頻拿過來,直接執行提取視頻第一幀的命令,提示圖片未提取成功,后來發現該視頻是產品同學通過某個壓縮工具壓縮過的,點開視頻可以看見黑屏,看不到任何東西,肯定是壓縮時把視頻壓縮出錯了,但是截取首幀命令既然執行結束了,按道理不應該一直堵塞啊?
於是通過dump下了內存鏡像文件,命令jmap -dump:live,format=b,file=heap.dmp PID,通過jvisualvm工具查看,發現有很多如下的堆棧:
因此可以判斷,確實是在截取首幀的時候,進程阻塞了,但是為什么會阻塞???
解決方案
查看waitFor()源碼可以發現,其實調用的是Object類中的,wait()方法,並且未指定等待時間,那么如果一直不返回,則會一直阻塞。
並且查看了JDK的幫助文檔,如下
因此,可以得出結論:如果外部程序不斷在向標准輸出流(對於jvm來說就是輸入流)和標准錯誤流寫數據,而JVM不讀取的話,當緩沖區滿之后將無法繼續寫入數據,最終造成阻塞在waitFor()這里。
解決方法:在waitFor()之前,利用單獨兩個線程,分別處理process的getInputStream()和getErrorSteam(),防止緩沖區被撐滿,導致阻塞;
/**
* 處理process輸出流和錯誤流,防止進程阻塞
* 在process.waitFor();前調用
* @param process
*/
private static void dealStream(Process process) {
if (process == null) {
return;
}
// 處理InputStream的線程
new Thread() {
@Override
public void run() {
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
try {
while ((line = in.readLine()) != null) {
logger.info("output: " + line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
// 處理ErrorStream的線程
new Thread() {
@Override
public void run() {
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line = null;
try {
while ((line = err.readLine()) != null) {
logger.info("err: " + line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
err.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
}
更多網易技術、產品、運營經驗分享請點擊。