新手學習FFmpeg - 通過API完成filter-complex功能
本篇嘗試通過API實現Filter Graph功能。 原始碼請參看 https://andy-zhangtao.github.io/ffmpeg-examples/
FFmpeg提供了很多實用且強大的濾鏡,比如:overlay, scale, trim, setpts等等。
通過-filter-complex
的表示式功能,可以將多個濾鏡組裝成一個呼叫圖,實現更為複雜的視訊剪輯。如何通過程式碼實現這個功能呢?
首先按照前面幾篇的套路,在開發FFmpeg應用時,大致有三板斧:
- 初始化輸入裝置(初始化解碼器及其應用上下文)
- 初始化輸出裝置(初始化編碼器及其應用上下文)
- 編寫幀處理邏輯(對符合要求的幀資料做各種運算處理)
本次需要實現的Filter Graph功能稍有不同,在處理幀之前需要先完成Filter Graph
的處理。 處理流程如下:
+------------------------------------------------+ | +---------+ | | | Input | ----------read --------+ | | +---------+ | | | | | | \|/ | | +-----------+ | | +-----------------------| Input | | | | +-----------| | | | | | | | \|/ | | | +-----------+ +-----------+ | | +<--| Filter N |<-.N.--| Filter 1 | | | | +-----------+ +-----------+ | | | | | | +-------------+ | | +------>| Output | | | +-------------+ | +------------------------------------------------+
從Input
讀取到視訊資料之後,會依次經過Filter 1
和Filter N
,每個Filter會依次根據設定好的引數處理流經的幀資料,當所有Filter都處理完畢之後,再經過編碼器編碼吸入Output
.
從流程可以看出,視訊中的每一幀都被處理了N次,這也是視訊在應用濾鏡時感覺編解碼時間有些長的原因。
本次增加了一部分API:
- avfilter_get_by_name
- avfilter_inout_alloc
- avfilter_graph_alloc
- avfilter_graph_create_filter
- avfilter_graph_parse_ptr
- av_buffersink_get_frame
- 初始化出入裝置
和以前的操作一樣,這裡就不做過多敘述。若有需要可以翻看前幾篇文章。這裡只增加一個dump函式:
av_dump_format(inFormatContext, 0, "1", 0);
av_dump_format
可以輸出指定FormatContext的資料,方便定位問題。
- 初始化輸出裝置
同樣不做過多描述,若有需要可翻看前幾篇文章或者直接看原始碼。 僅僅提醒一下關於time_base的幾個坑。
time_base是用來做基準時間轉換的,也就是告訴編碼器以何種速度來播放幀(也就是pts)。前幾篇程式碼中所使用的time_base是:
outCodecContext->time_base = (AVRational) {1, 25};
1是分子,25是分母。 在進行編碼時,編碼器需要知道每一個關鍵幀要在哪個時間點進行展示和渲染(對應的就是pts和dts)。 在沒有B幀的情況下,PTS=DTS。 而計算pts時,需要建立編碼time_base和解碼time_base的對應關係.
假設,time=5. 那麼在1/25(編碼time_base)的時間刻度下應該等於1/10000(編碼time_base)時間刻度下的(5*1/25)/(1/90000) = 3600*5=18000
time_base的詳細應用,可以參考setpts
中的實現。
- 初始化Filter Graph
在Filter Graph API
中有兩個特殊的Filter:buffer
和buffersink
:
----------> |buffer| ---------|Filter ..... Filter N|----------->|buffersink|-------->
buffer
表示Filter Graph的開始,buffersink
表示Filter Graph的結束。這兩中Filter是必須要存在不可缺少。
Filter Graph使用的步驟如下:
- 初始化
buffer
和buffersink
。 - 初始化其它filter
- 設定Filter Graph的Input和Output。
- 初始化
buffer
和buffersink
通過avfilter_get_by_name
來查詢相符的Filter,例如:
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
表示獲取buffer Filter。然後通過avfilter_graph_create_filter來初始化filter,例如初始化buffer:
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
inCodecContext->width, inCodecContext->height, inCodecContext->pix_fmt,
time_base.num, time_base.den,
inCodecContext->sample_aspect_ratio.num, inCodecContext->sample_aspect_ratio.den);
av_log(NULL, AV_LOG_ERROR, "%s\n", args);
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
"in"表示buffer在整個Graph中叫做'in'。 名稱可以隨便叫,只要保證唯一不重複就好。
- 初始化其它filter
通過``使用指定的Filter Graph 語法來初始化剩餘的Filter,例如:
const char *filter_descr = "movie=t.png[wm];[in][wm]overlay=10:20[out]";
avfilter_graph_parse_ptr(filter_graph, filter_descr,
&inputs, &outputs, NULL)
上面表示使用了兩個filter:movie
和overlay
。 inputs
和outputs
表示Graph的輸入輸出。
- 設定Filter Graph的Input和Output
這段程式碼有些不好理解:
outputs->name = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
inputs->name = av_strdup("out");
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
outputs對應的是in
(也就是buffer),in
是Graph第一個Filter,所以它只有輸出端(所以對應到了outputs)。 同理out
(buffersink)是Graph最後一個Filter,只有輸入端,因此對應到了inputs。
+-------+ +---------------------+ +---------------+
|buffer | |Filter ..... Filter N| | buffersink |
----------> | |output|------>|input| |output|---> |input| |-------->
+-------+ +---------------------+ +---------------+
在下一篇中,我們會通過其它api設定每個Filter的input和output,那個時候應該會更容易理解一點。
在完成Filter Graph初始化之後,一定要通過avfilter_graph_config
來驗證引數配置是否正確。
avfilter_graph_config(filter_graph, NULL)
- 邏輯處理
在處理幀資料時,就和以前的思路基本保持一致了。 從解碼器接受幀,然後傳送到Filter Graph
中進行濾鏡處理,最後再發送給編碼器寫入到輸出檔案。
唯一有些不同的就是增加了兩個函式av_buffersrc_add_frame_flags
和av_buffersink_get_frame
. av_buffersrc_add_frame_flags
表示向Filter Graph加入一幀資料,av_buffersink_get_frame
表示從Filter Graph取出一幀資料。
因此上一篇中的編碼流程增加了一個while迴圈:
while av_read_frame
|
+---> avcodec_send_packet
|
+----> while avcodec_receive_frame
| 對每一資料幀進行解碼
| 通過`sws_scale`進行源幀和目標幀的資料轉換
|
+---->av_buffersrc_add_frame_flags
|
|
+while av_buffersink_get_frame
|
|
+-->avcodec_send_frame
|
+---> while avcodec_receive_packet
|
|
|+--->av_interleaved_write_frame (寫入到輸出裝置)
至此就完成了通過程式碼實現-filter-complex
功能