一. ffmpeg AVFilter介紹
利用ffmpeg做圖像的pixel format轉換你還在用libswscale嗎?嘿嘿,過時啦!
ffmpeg中有了新東西:libavfilter.使用它,可以完全代替libswscale,並且可以自動完成一些復雜的轉換操作呢.libavfilter啊,用了都說好!但就是太復雜...
如果你僅僅是做圖像的pixel format處理,用libswscale是相當簡單,可以看看最新的ffplay.c中的代碼,被#if CONFIG_AVFILTER #endif包圍的代碼量非常大,而且讓人一上來看得一頭霧水,但為了趕潮流,我們還是得學習它啊...
先弄清楚avfilter中的幾個相關的概念(注意:如果沒有directShow基礎的同學看不懂以下解釋,請先學DirectShow的基本概念):
1 AVFilterGraph:幾乎完全等同與directShow中的fitlerGraph,代表一串連接起來的filter們.
AVFilter:代表一個filter.
AVFilterPad:代表一個filter的輸入或輸出口,等同於DShow中的Pin.只有輸出pad的filter叫source,只有輸入pad的tilter叫sink.
AVFilterLink:代表兩個連接的fitler之間的粘合物.
其實總體看起來,libavfitler跟DShow幾乎一樣了.
下面看一下AVFilter是如何被使用的,我們以ffplay.c為例吧,分析一下其中AVFilter相關的代碼.
1 產生graph:
AVFilterGraph *graph = avfilter_graph_alloc();
2 創建source
AVFilterContext *filt_src;
avfilter_graph_create_filter(&filt_src, &input_filter, "src",NULL, is, graph);
第一個參數是生成的filter(是一個source),第二個參數是一個AVFilter結構的實例,第三個參數是要創建的fitler的名字,第四個參數是不知道什么用,第五個參數是user data(調用者的私有數據),第六個參數是graph的指針.其中第二個參數的實例必須由調用者自己實現,才能將幀送到graph中.
3 創建sink
AVFilterContext *filt_out;
ret = avfilter_graph_create_filter(&filt_out, avfilter_get_by_name("buffersink"), "out", NULL, pix_fmts, graph);
參數同上,不解釋.所創建的這個sink是一個buffersink,可參考libavfitler的源碼文件sink_buffer.c看看它是個什么玩意.sink_buffer其實是一個能通過buffer輸出幀的sink,當然它的輸出不是通過pad,因為它后面沒有fitler了.用它做sink,可以讓使用這個graph的代碼輕松取得graph處理后的幀.
4 連接source和sink
avfilter_link(filt_src, 0, filt_out, 0);
第一個參數是接在前面的filter,第二個參數是前fitler的要連接的pad的序號,第三個參數是后面的filter,第四個參數是后filter的要連接的pad.
4 對graph做最后的檢查
avfilter_graph_config(graph, NULL);
我們是從sink中取出處理完成的幀,所以最好把sink的引用保存下來,比如:
AVFilterContext *out_video_filter=filt_out;
6實現input_filter
由於input_filter是個source,所以只為它分配output pad,並且只有一個pad.
- static AVFilter input_filter =
- {
- .name = "ffplay_input",
-
- .priv_size = sizeof(FilterPriv),
-
- .init = input_init,
- .uninit = input_uninit,
-
- .query_formats = input_query_formats,
-
- .inputs = (AVFilterPad[]) {{ .name = NULL }},
- .outputs = (AVFilterPad[]) {{ .name = "default",
- .type = AVMEDIA_TYPE_VIDEO,
- .request_frame = input_request_frame,
- .config_props = input_config_props, },
- { .name = NULL }},
- };
再實現AVFilter的回調函數們:init()和uninit()--用於初始化/銷毀所用到的資源.
看一下ffplay.c中的實現:
- static int input_init(AVFilterContext *ctx, const char *args, void *opaque)
- {
- FilterPriv *priv = ctx->priv;
- AVCodecContext *codec;
- if(!opaque) return -1;
-
- priv->is = opaque;
- codec = priv->is->video_st->codec;
- codec->opaque = ctx;
- if((codec->codec->capabilities & CODEC_CAP_DR1)) {
- av_assert0(codec->flags & CODEC_FLAG_EMU_EDGE);
- priv->use_dr1 = 1;
- codec->get_buffer = input_get_buffer;
- codec->release_buffer = input_release_buffer;
- codec->reget_buffer = input_reget_buffer;
- codec->thread_safe_callbacks = 1;
- }
-
- priv->frame = avcodec_alloc_frame();
-
- return 0;
- }
FilterPriv是ffplay實現的filter(也就是input_filter)的私有數據結構.主要的工作是分配了一個AVFrame,用於保存從設備取得的幀.uninit()更簡單,就不用看了.
還需實現output pad的request_frame(),才能使input_filter后面的filter獲取到幀
- static int input_request_frame(AVFilterLink *link)
- {
- FilterPriv *priv = link->src->priv;
- AVFilterBufferRef *picref;
- int64_t pts = 0;
- AVPacket pkt;
- int ret;
-
- while (!(ret = get_video_frame(priv->is, priv->frame, &pts, &pkt)))
- av_free_packet(&pkt);
- if (ret < 0)
- return -1;
-
- if(priv->use_dr1 && priv->frame->opaque) {
- picref = avfilter_ref_buffer(priv->frame->opaque, ~0);
- } else {
- picref = avfilter_get_video_buffer(link, AV_PERM_WRITE, link->w, link->h);
- av_image_copy(picref->data, picref->linesize,
- priv->frame->data, priv->frame->linesize,
- picref->format, link->w, link->h);
- }
- av_free_packet(&pkt);
-
- avfilter_copy_frame_props(picref, priv->frame);
- picref->pts = pts;
-
- avfilter_start_frame(link, picref);
- avfilter_draw_slice(link, 0, link->h, 1);
- avfilter_end_frame(link);
-
- return 0;
- }
調用者從sink中獲取處理后的幀:
av_buffersink_get_buffer_ref(filt_out, &picref, 0);
獲取后的幀保存在picref中.這個函數會引起graph中的filter從后向前依次調用上一個filter的outpad的request_frame(),最后調用到source的request_frame(),也就是input_request_frame(),input_request_frame()調用get_video_frame()(見ffplay.c)從設備獲取一幀(可能需要解碼),然后將這幀數據復制到picref中,filter們處理的幀是用AVFilterBufferRef表示的.然后將幀的一些屬性也復制到picref中,最后調用avfilter_start_frame(link, picref);avfilter_draw_slice(link, 0, link->h, 1);avfilter_end_frame(link);來處理這一幀.這三個函數對應着pad上的三個函數指針:start_frame,draw_slice,end_frame.以start_frame為例,其調用過程是這樣的:首先是source的start_frame被調用,做一些必要的處理后,再調用連接到source之后的filter的start_frame.每個filter的output pad都負責在這個函數中向下傳遞這個調用.當sink調用完start_frame()時再一層層返回到source的output pad.當這三個函數都被source的output pad調用完成后,這一幀的最終結果就出來了.於是可以用sink上獲得.
與DShow比較起來,avfilter沒有那些推模式,拉模式的概念,沒有在source的output pad上實現線程,整個graph的運轉都是由調用者驅動.
轉自http://blog.csdn.net/nkmnkm/article/details/7219641
二. FFmpeg filter HOWTO
定義一個濾鏡
AVFilter
所有我們寫的濾鏡都要用一個AVFilter結構體講給ffmpeg聽。 這個結構體里描述了ffmpeg從哪個方法進入我們的濾鏡。 這個結構體在libavfilter/avfilter.h里如下定義:
07 |
int (*init)(AVFilterContext *ctx, const char *args, void *opaque); |
08 |
void (*uninit)(AVFilterContext *ctx); |
10 |
int (*query_formats)(AVFilterContext *ctx); |
12 |
const AVFilterPad *inputs; |
13 |
const AVFilterPad *outputs; |
“query_formats”方法用於設置可以接受的輸入圖像格式和輸出的圖像格式(用於濾鏡鏈分辨哪些濾鏡可以組合在一起用)。
AVFilterPad
這個濾鏡用於描述濾鏡的輸入輸出,在libavfilter/avfilter.h中定義如下:
01 |
typedef struct AVFilterPad |
09 |
void (*start_frame)(AVFilterLink *link, AVFilterPicRef *picref); |
10 |
AVFilterPicRef *(*get_video_buffer)(AVFilterLink *link, int perms); |
11 |
void (*end_frame)(AVFilterLink *link); |
12 |
void (*draw_slice)(AVFilterLink *link, int y, int height); |
14 |
int (*request_frame)(AVFilterLink *link); |
16 |
int (*config_props)(AVFilterLink *link); |
頭文件里有十分具體的描述,這里大概解釋如下:
輸入輸出pad都可以用的元素:
name pad的名字,所有的輸入pad名字不能重復,所有的輸出名字不能重復;
type 此元素目前只能為“AV_PAD_VIDEO”值
config_props 鏈接此pad的配置方法的函數指針
僅限輸入pad使用的元素:
min_perms 接受輸入需要的最小權限
rej_perms 不接受的輸入權限
start_frame 一幀傳入時引用的方法的函數指針
draw_slice 每個slice已經傳入后引用的方法的函數指針
end_frame 一幀完整結束后引用的方法的函數指針
get_video_buffer 前一個濾鏡調用,用以為一個圖像請求內存
僅限輸出pad使用的元素:
request_frame 請求濾鏡輸出一幀
圖像緩沖
引用計數
濾鏡系統使用引用計數。意味着存在一個buffer里面存放着圖像數據,而所有的濾鏡都各自保有一個指向這個buffer的引用。當每一個濾鏡完事,它就釋放自己的那個引用。這樣當所有的引用都釋放以后,濾鏡系統就會自動把buffer釋放掉。
權限
由於可能有多個濾鏡都保有了buffer的指針,它們可能會同時操作buffer而造成沖突,因此ffmpeg引入了權限系統。
大多數情況下,當一個濾鏡准備輸出一幀時,它調用濾鏡鏈上下一個濾鏡的一個方法來請求一個buffer。這指定了它對這個buffer需要的最低權限,不過可能實際被賦予的權限可能比要求的要高。
在想要把buffer輸出給另一個濾鏡時,會新建一個新的指向這個圖像的引用,可能是一個權限標記的子集。這個新的引用屬於接受buffer的濾鏡。
舉例說:一個丟幀的濾鏡在輸出最后一幀時,他可能在輸出之后還是想要保持一個指向圖像的引用,以確保沒有其它濾鏡同時修改這個buffer。為了達到這個目的,他可能請給自己求AV_PERM_READ|AV_PERM_WRITE|AV_PERM_PRESERVE權限,然后在給予其它濾鏡的引用里去掉AV_PERM_WRITE權限。
可用的權限有:
AV_PERM_READ 可以讀取圖像數據
AV_PERM_WRITE 可以寫入圖像數據
AV_PERM_PRESERVE 保證圖像數據不會被其它濾鏡修改,意味着不會有其它濾鏡拿到AV_PERM_WRITE權限
AV_PERM_REUSE 濾鏡可能往一段buffer多次輸出,但圖像數據不得切換到不同的輸出
AV_PERM_REUSE2 濾鏡可能往一段buffer多次輸出,可能在不同的輸出之間修改圖像數據
濾鏡鏈
濾鏡的輸入輸出用“AVFilterLink”結構體和其它濾鏡相連接:
01 |
typedef struct AVFilterLink |
11 |
enum PixelFormat format; |
13 |
AVFilterFormats *in_formats; |
14 |
AVFilterFormats *out_formats; |
16 |
AVFilterPicRef *srcpic; |
18 |
AVFilterPicRef *cur_pic; |
19 |
AVFilterPicRef *outpic; |
成員“src”和“dst”分別指出濾鏡在鏈上的輸入和輸出的結束。“srcpad”指向鏈條上一個“源濾鏡”的輸出面的索引;類似的,“dstpad”指向目標濾鏡的輸入面的索引。
成員“in_formats”指向“源濾鏡”定義的它支持的格式,“out_formats”指向“目標濾鏡”支持的格式。結構體“AVFilterFormats”用於存儲支持格式的列表,它使用引用計數,跟蹤它的引用(參見libavfilter/avfilter.h中關於AVFilterFormats結構體的注釋,了解色度空間的協商機制是怎么工作的,以及為什么協商是必要的)。結果就是一個濾鏡如果為它之前和之后的濾鏡提供了指向相同支持格式的列表的指針,就意味着這個鏈條上的濾鏡就只能使用相同的格式了。
兩個濾鏡相連時,它們需要在它們處理的圖像數據的尺寸和圖像格式上達成一致。達成一致后,這些會作為參數存儲在link結構體中。
成員“srcpic”是濾鏡系統內部使用的,不應該直接存取。
成員“cur_pic”是給目標濾鏡用的。當一個幀正在通過濾鏡鏈時(開始於start_frame(),結束於end_frame),這個成員包含了目標濾鏡對此幀的引用。
成員“outpic”會在接下來一個小教程中詳細介紹。
寫一個簡單的濾鏡
默認的濾鏡入口點
因為大多數濾鏡都只有一個輸入一個輸出,且每接受一個幀只輸出一個幀,ffmpeg的濾鏡系統提供了一系列默認的切入點以簡化這種濾鏡的開發,以下是切入點和默認實現的作用:
request_frame() 從濾鏡鏈中前一個濾鏡那請求一個幀
query_formats() 設置所有面上都支持的格式列表,這樣別人就要按照這個列表來。默認包含大多數的YUV和RGB/BGR格式
start_frame() 請求一個buffer來保存輸出幀。一個指向此buffer的引用存儲在hook到濾鏡的輸出的link的“outpic”成員中。下一個濾鏡的start_frame()回調會被調用,傳入一個此buffer的引用。
end_frame() 調用下一個濾鏡的end_frame()回調函數。釋放指向輸出link的“outpic”成員的引用,如果那成員被設置了(比如說使用了默認的start_frame()方法)。釋放輸入link的“cur_pic”引用
get_video_buffer() 返回一個在要求的權限上加一個AV_PERM_READ權限的buffer
config_props() on output pad 把輸出的圖像尺寸設置成和輸入一樣
“vf_negate”濾鏡
介紹了數據結構和回調函數,讓我們來看一個真實的濾鏡。vf_negate濾鏡的效果是反轉視頻中的色彩。它就一個輸入一個輸出,並且每個輸入幀都輸出一個幀。非常典型,可以使用濾鏡系統那些默認的回調實現。
首先,讓我們看一眼在“libavfilter/vf_negate.c”文件最下面的“AVFilter”結構體:
01 |
AVFilter avfilter_vf_negate = |
05 |
.priv_size = sizeof (NegContext), |
07 |
.query_formats = query_formats, |
09 |
.inputs = (AVFilterPad[]) {{ .name = "default" , |
11 |
.draw_slice = draw_slice, |
12 |
.config_props = config_props, |
13 |
.min_perms = AV_PERM_READ, }, |
15 |
.outputs = (AVFilterPad[]) {{ .name = "default" , |
16 |
.type = AV_PAD_VIDEO, }, |
可以看到濾鏡的名字是“negate”,需要sizeof(NegContext)字節的空間存儲上下文。在input和output列表的最后,都有一個name設置為NULL的pad。可以看出這個濾鏡確實只有一個輸入一個輸出。如果你仔細觀察pad的定義,你會發現好多回調函數已經被指定好了。因為我們這個濾鏡很簡單,所以大多數保持默認的就可以。
讓我們看看它自己定義的回調函數。
query_formats()
01 |
static int query_formats(AVFilterContext *ctx) |
03 |
avfilter_set_common_formats(ctx, |
04 |
avfilter_make_format_list(10, |
05 |
PIX_FMT_YUV444P, PIX_FMT_YUV422P, PIX_FMT_YUV420P, |
06 |
PIX_FMT_YUV411P, PIX_FMT_YUV410P, |
07 |
PIX_FMT_YUVJ444P, PIX_FMT_YUVJ422P, PIX_FMT_YUVJ420P, |
08 |
PIX_FMT_YUV440P, PIX_FMT_YUVJ440P)); |
這個函數調用了avfilter_make_format_list()。這個方法第一個函數指定后面要列舉多少個格式,后面就把格式列出來。返回是一個包含指定格式的AVFilterFormats結構體。把這個結構體傳給avfilter_set_common_formats()方法把格式給設置上。如同你看到的,這個濾鏡支持一堆YUV平面的色彩空間格式,包括JPEG YUV色彩空間(其中那些包含字母J的)。
config_props() on an input pad
input填充的config_props()負責驗證是否支持輸入pad的屬性,也負責更新濾鏡的屬性上下文。
TODO: 快速解釋一下YUV色彩空間,色讀采樣,YUV和JEPG YUV范圍的不同。
讓我們看看濾鏡是怎么存儲它的上下文的:
AVFilter結構體中的成員“priv_size”告訴濾鏡系統它需要多少字節來存儲這個結構體。成員“hsub”和“vsub”用於色度采樣,成員“offY”和“offUV”用於YUV和JPEG間范圍的不同。讓我們看看這些在輸入pad的config_props中是咋設置的:
01 |
static int config_props(AVFilterLink *link) |
03 |
NegContext *neg = link->dst->priv; |
05 |
avcodec_get_chroma_sub_sample(link->format, &neg->hsub, &neg->vsub); |
07 |
switch (link->format) { |
08 |
case PIX_FMT_YUVJ444P: |
09 |
case PIX_FMT_YUVJ422P: |
10 |
case PIX_FMT_YUVJ420P: |
11 |
case PIX_FMT_YUVJ440P: |
它只是簡單調用了avcodec_get_chroma_sub_sample()方法去得到色度采樣的位移因子,然后把它們存到上下文中。然后它存儲了一些JPEG YUV的亮度/色度范圍不同的偏移補償。返回0表明成功,因為沒有這個濾鏡無法處理的輸入格式。
draw_slice()
最后,濾鏡中最重要的方法,它實際處理圖像,draw_slice():
01 |
static void draw_slice(AVFilterLink *link, int y, int h) |
03 |
NegContext *neg = link->dst->priv; |
04 |
AVFilterPicRef *in = link->cur_pic; |
05 |
AVFilterPicRef *out = link->dst->outputs[0]->outpic; |
06 |
uint8_t *inrow, *outrow; |
10 |
inrow = in-> data[0] + y * in-> linesize[0]; |
11 |
outrow = out->data[0] + y * out->linesize[0]; |
12 |
for (i = 0; i < h; i ++) { |
13 |
for (j = 0; j < link->w; j ++) |
14 |
outrow[j] = 255 - inrow[j] + neg->offY; |
15 |
inrow += in-> linesize[0]; |
16 |
outrow += out->linesize[0]; |
20 |
for (plane = 1; plane < 3; plane ++) { |
21 |
inrow = in-> data[plane] + (y >> neg->vsub) * in-> linesize[plane]; |
22 |
outrow = out->data[plane] + (y >> neg->vsub) * out->linesize[plane]; |
24 |
for (i = 0; i < h >> neg->vsub; i ++) { |
25 |
for (j = 0; j < link->w >> neg->hsub; j ++) |
26 |
outrow[j] = 255 - inrow[j] + neg->offUV; |
27 |
inrow += in-> linesize[plane]; |
28 |
outrow += out->linesize[plane]; |
32 |
avfilter_draw_slice(link->dst->outputs[0], y, h); |
“y”參數是當前slice的頂部,“h”參數是slice的高度。在這個區域以外的圖像被假設為是無意義的(可能在未來的一些濾鏡中這個假設會被打破)。
變量“inrow”指向輸入slice的第一行,“outrow”指向輸出的第一行。然后,它先遍歷每一行,然后在每一行中遍歷每一個像素,用255去減像素值,加上在config_props()中為不同格式的范圍做出的修正值。
然后它在色度平面上做了同樣的事。注意寬度和高度是調整到合適色度采樣的。
在圖像修改結束后,調用calling avfilter_draw_slice()方法把slice送給下一個濾鏡去處理。
翻譯自
http://wiki.multimedia.cx/index.php?title=FFmpeg_filter_HOWTO