音視訊點播服務基礎系列(Fmpeg常用命令)

odirus發表於2021-06-25

前言

  公司業務中有一些場景需要用到服務端音視訊剪輯技術,最開始為了快速上線使用的是某公有云的商用解決方案,但由於費用太高所以我們團隊經過一個星期的衝刺,給出了一個FFmpeg+Serverless的解決方案,更好地滿足了業務方的視訊剪輯需求。經過統計,自研方案成功地將剪輯失敗率降到了萬分之一左右,並且將費用成本降到了原本的15%左右,是一個非常大的突破。現在我們計劃把該平臺做得更加通用化,讓更多的業務可以無縫接入,通過任務編排來實現更多定製化需求。

  題外話,為什麼我們會選擇Serverless來實現該業務呢,因為我們的業務高峰期特別明顯,時效性要求也高,在使用某公有云解決方案期間經常觸發系統QPS限制,經過多次溝通也只能臨時調整,而且對方技術半夜還打電話來問我是否可以限制QPS,作為使用方肯定是不願意的,所以除了成本外效能也是我們下決心自研系統的原因之一。Serverless在使用過程中遇到的問題我也會在後續時間裡面記錄下來。

  本編部落格主要是記錄在整個過程中涉及到的FFmpeg常見命令,以及一些坑,分享給大家使用,若有謬誤請批評指正。

 

基本命令

音視訊剪下

String.format("%s -i %s -vcodec libx264  -ss %s -to %s %s -y", ffmpegCommandPath, sourceFilePath, startTime, endTime, targetFilePath)

ffmpegCommandPath    表示FFmpeg執行命令的路徑

sourceFilePath              表示原始檔路徑

startTime                     表示剪下的起始點,格式為 "00:00:00", 例如 "00:00:15" 表示剪下從第15秒開始

endTime                      表示剪下的終止點,格式為 "00:00:00", 例如 "00:00:20" 表示剪下截止到第20秒

targetFilePath               表示剪下後的輸出檔案

-y                               表示輸出檔案若存在則覆蓋  

 

音訊/視訊簡單拼接

String.format("%s -f concat -safe 0 -i %s -c copy %s", ffmpegCommandPath, concatListFilePath, destinationFilePath)

ffmpegCommandPath    表示FFmpeg執行命令的路徑

concatListFilePath         表示拼接的配置檔案,內容格式為

file 'filePath1.audio'

file 'filePath2.audio'

destinationFilePath        表示拼接後的輸出檔案

使用限制:該方式不涉及到檔案的解碼、編碼,所以速度極快,但如果待處理檔案的編碼格式不同則請勿使用,否則輸出的檔案可能無法正常播放(或者只能播放一部分)。如果編碼格式不同,請參考下文中的音訊拼接/視訊拼接方式,會更加可靠,當會更加消耗資源。

 

音訊拼接

由於涉及到到引數拼接,所以直接上程式碼(Java方式)。

/**
 * 音訊檔案拼接
 * @param files                     音訊檔案資源路徑陣列
 * @param destinationFilePath       處理後輸出檔案路徑
 */
public static void audioConcat(String[] files, String destinationFilePath) {
    // command list
    List<String> commandList = new ArrayList<>();
    commandList.add("ffmpeg");

    // input_options
    for (String file : files) {
        commandList.add("-i");
        commandList.add(file);
    }

    // filter_complex
    StringBuilder filterComplexOptions = new StringBuilder();
    for (int i = 0; i < files.length; i++) {
        filterComplexOptions.append(String.format("[%s:0]", i));
    }
    filterComplexOptions.append(String.format("concat=n=%s:v=0:a=1[out]", files.length));
    commandList.add("-filter_complex");
    commandList.add(filterComplexOptions.toString());
    commandList.add("-map");
    commandList.add("[out]");
    commandList.add(destinationFilePath);
    Runtime.getRuntime().exec(commandList.toArray(new String[0]));

    // next process
}

 

視訊拼接

由於涉及到到引數拼接,所以直接上程式碼(Java方式)。

/**
 * 視訊拼接
 * @param files                  音訊檔案資源路徑陣列
 * @param destinationFilePath    處理後輸出檔案路徑
 * @param outputWidth            輸出視訊的寬度
 * @param outputHeight           輸出視訊的高度
 */
public static void videoConcat(String[] files, String destinationFilePath, Integer outputWidth, Integer outputHeight) {
    // command list
    List<String> commandList = buildFfmpegCommand();
    commandList.add("ffmpeg");

    // input_options
    for (String file : files) {
        commandList.add("-i");
        commandList.add(file);
    }

    // filter_complex
    StringBuilder filterComplexOptions = new StringBuilder();
    StringBuilder streamsOptions = new StringBuilder();
    for (int i = 0; i < files.length; i++) {
        filterComplexOptions.append(String.format("[%s:v]scale=w=%s:h=%s,setsar=1/1[v%s];", i, outputWidth, outputHeight, i));
        streamsOptions.append(String.format("[v%s][%s:a]", i, i));
    }
    streamsOptions.append(String.format("concat=n=%s:v=1:a=1 [vv] [aa]", files.length));
    commandList.add("-filter_complex");
    commandList.add(String.format("%s%s", filterComplexOptions.toString(), streamsOptions.toString()));
    Collections.addAll(commandList, "-map", "[vv]", "-map", "[aa]", "-c:v", "libx264", "-x264-params",
        "profile=main:level=3.1", "-crf", "18", "-y", "-vsync", "vfr");
    commandList.add(destinationFilePath);
    Runtime.getRuntime().exec(commandList.toArray(new String[0]));

    // next process
}

踩坑經驗: 我們在拼接過程中遇到了視訊拼接出錯的情況,但數量比較少,通過FFprobe命令分析,發現這種情況出現在其中某個視訊無音軌的情況,找了很多解決方案,最後採用的方式是為這個視訊配一個音軌,相當於先把素材標準化處理,為視訊注入音軌的方式見下文 "無音軌視訊配音"。

 

音視訊混合

String.format("%s -i %s -i %s -filter_complex amix -map 0:v -map 0:a -map 1:a -shortest -y %s", ffmpegCommandPath, videoFilePath, audioFilePath, targetFilePath)

ffmpegCommandPath    表示FFmpeg執行命令的路徑

videoFilePath                表示視訊檔案路徑

audioFilePath                表示音訊檔案路徑

targetFilePath               表示輸出檔案路徑

 

無音軌視訊配音

String.format("%s -i %s -f lavfi -i aevalsrc=0 -shortest -y %s", ffmpegCommandPath, videoFilePath, targetFilePath)

ffmpegCommandPath    表示FFmpeg執行命令的路徑

videoFilePath                表示視訊檔案路徑

targetFilePath               表示輸出檔案路徑

 

踩坑經驗

Runtime.getRuntime().exec() 的問題

上述涉及到Java的部分都是採用的 Runtime.getRuntime().exec(String[] cmdarray) 而不是 Runtime.getRuntime().exec(String command),因為後者一旦遇到雙引號就會帶來問題,當時就想使用字串的方式來執行(這樣的話一旦程式中遇到問題就可以很方便地複製到Shell中復現),但命令中一旦存在雙引號就無法解決,困擾了很久,讀到了 "getruntime() exec() with double quotes in command" 這篇文章後決定就用陣列形式吧。

 

執行結果 Process.waitFor() 的問題

Runtime.getRuntime().exec() 的執行結果需要通過 waitFor() 方式來獲取子程式退出碼,以此來判斷是否執行成功,但倘若 FFmpeg 沒有關閉除錯資訊,則可能會導致該函式一直卡在這。當時程式中有少部分任務會卡死,我還以為是Serverless平臺的問題,但後面通過 "Java process.waitFor() 卡死問題" 這篇文章找到了解決方案,因此可以通過關閉除錯資訊的方式來規避,即"ffmpeg -loglevel quiet"。

相關文章