在上一篇文章中,講解了如何利用ffmpeg實(shí)現(xiàn)攝像頭直播,本文將在此基礎(chǔ)上,實(shí)現(xiàn)一個(gè)可以選擇各種視頻濾鏡的攝像頭直播示例。本文包含以下內(nèi)容
1、AVFilter的基本介紹
2、如何利用ffmpeg命令行工具實(shí)現(xiàn)各種視頻濾鏡
3、如何利用libavfilter編程實(shí)現(xiàn)在攝像頭直播流中加入各類不同濾鏡的功能
具有較強(qiáng)的綜合性。
AVFilter的基本介紹
AVFilter的功能十分強(qiáng)大,可以實(shí)現(xiàn)對多媒體數(shù)據(jù)的各種處理,包括時(shí)間線編輯、視音頻特效濾鏡的添加或信號處理,還可以實(shí)現(xiàn)多路媒體流的合并或疊加,其豐富程度令人嘆為觀止。這里主要以視頻濾鏡為例進(jìn)行介紹。使用AVFilter可以為單路視頻添加單個(gè)或多個(gè)濾鏡,也可以為多路視頻分別添加不同的濾鏡并且在最后將多路視頻合并為一路視頻,AVFilter為實(shí)現(xiàn)這些功能定義了以下幾個(gè)概念:
Filter:代表單個(gè)filter
FilterPad:代表一個(gè)filter的輸入或輸出端口,每個(gè)filter都可以有多個(gè)輸入和多個(gè)輸出,只有輸出pad的filter稱為source,只有輸入pad的filter稱為sink
FilterLink:若一個(gè)filter的輸出pad和另一個(gè)filter的輸入pad名字相同,即認(rèn)為兩個(gè)filter之間建立了link
FilterChain:代表一串相互連接的filters,除了source和sink外,要求每個(gè)filter的輸入輸出pad都有對應(yīng)的輸出和輸入pad
FilterGraph:FilterChain的集合
基本和DirectShow類似,也與視頻后期調(diào)色軟件中的節(jié)點(diǎn)等概念類似。具體來看,以下面的命令為例
[in]split[main][tmp];[tmp]crop=iw:ih/2,vflip[flip];[main][flip]overlay=0:H/2[out]
在該命令中,輸入流[in]首先被分[split]為兩個(gè)流[main]和[tmp],然后[tmp]流經(jīng)過了剪切[crop]和翻轉(zhuǎn)[vflip]兩個(gè)濾鏡后變?yōu)閇flip],這時(shí)我們將[flip]疊加[overlay]到最開始的[main]上形成最后的輸出流[out],最后呈現(xiàn)出的是鏡像的效果。下圖清晰地表示了以上過程
我們可以認(rèn)為圖中每一個(gè)節(jié)點(diǎn)就是一個(gè)Filter,每一個(gè)方括號所代表的就是FilterPad,可以看到split的輸出pad中有一個(gè)叫tmp的,而crop的輸入pad中也有一個(gè)tmp,由此在二者之間建立了link,當(dāng)然input和output代表的就是source和sink,此外,圖中有三條FilterChain,第一條由input和split組成,第二條由crop和vflip組成,第三條由overlay和output組成,整張圖即是一個(gè)擁有三個(gè)FilterChain的FilterGraph。
上面的圖是人工畫出來的,也可以在代碼中調(diào)用avfilter_graph_dump函數(shù)自動(dòng)將FilterGraph畫出來,如下
可以看到,多出來了一個(gè)scale濾鏡,這是由ffmpeg自動(dòng)添加的用于格式轉(zhuǎn)換的濾鏡。
在FFmpeg命令行工具中使用AVFilter
在命令行中使用AVFilter需要遵循專門的語法,簡單來說,就是每個(gè)Filter之間以逗號分隔,每個(gè)Filter自己的屬性之間以冒號分隔,屬性和Filter以等號相連,多個(gè)Filter組成一個(gè)FilterChain,每個(gè)FilterChain之間以分號相隔。AVFilter在命令行工具中對應(yīng)的是-vf或-af或-filter_complex,前兩個(gè)分別對應(yīng)于單路輸入的視頻濾鏡和音頻濾鏡,最后的filter_complex則對應(yīng)于有多路輸入的情況。除了在FFMpeg命令行工具中使用外,在FFplay中同樣也可以使用AVFilter。其他一些關(guān)于單雙引號、轉(zhuǎn)義符號等更詳細(xì)的語法參考Filter
Documentation
下面舉幾個(gè)例子
1、疊加水印
ffmpeg -i test.flv -vf movie=test.jpg[wm];[in][wm]overlay=5:5[out] out.flv
將test.jpg作為水印疊加到test.flv的坐標(biāo)為(5,5)的位置上,效果如下
2、鏡像
ffmpeg -i test.flv -vf crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left]pad=iw*2[a];[a][right]overlay=w out.flv
輸入[in]和輸出[out]可以省略不寫,pad用于填充畫面,效果如下
3、調(diào)整曲線
ffmpeg -i test.flv -vf curves=vintage out.flv
類似Photoshop里面的曲線調(diào)整,這里的vintage是ffmpeg自帶的預(yù)設(shè),實(shí)現(xiàn)復(fù)古畫風(fēng),還可以直接加載其他的Photoshop預(yù)設(shè)文件并在其基礎(chǔ)上加以調(diào)整,如下
ffmpeg -i test.flv -vf curves=psfile='test.acv':green='0.45/0.53' out.flv
其中的acv預(yù)設(shè)文件實(shí)現(xiàn)的是加強(qiáng)對比度,再次基礎(chǔ)上調(diào)整綠色的顯示效果,以上兩個(gè)命令的最終效果如下
4、多路輸入拼接
ffmpeg -i test1.mp4 -i test2.mp4 -i test3.mp4 -i test4.mp4 -filter_complex "[0:v]pad=iw*2:ih*2[a];[a][1:v]overlay=w[b];[b][2:v]overlay=0:h[c];[c][3:v]overlay=w:h" out.mp4
正如前面所說的,當(dāng)有多個(gè)輸入時(shí),需要使用filter_complex,效果如下
通過以上幾個(gè)例子,基本可以明白在命令行中使用AVFilter時(shí)需要遵循的語法。
使用libavfilter編程為直播流添加濾鏡
要使用libavfilter,首先要注冊相關(guān)組件
avfilter_register_all();
首先需要構(gòu)造出一個(gè)完整可用的FilterGraph,需要用到輸入流的解碼參數(shù),參見上一篇文章,如下
AVFilterContext *buffersink_ctx;//看名字好像AVFilterContext是什么很厲害的東西,但其實(shí)只要認(rèn)為它是AVFilter的一個(gè)實(shí)例就OK了AVFilterContext *buffersrc_ctx;AVFilterGraph *filter_graph;AVFilter *buffersrc=avfilter_get_by_name("buffer");//Filter的具體定義,只要是libavfilter中已注冊的filter,就可以直接通過查詢filter名字的方法獲得其具體定義,所謂定義即filter的名稱、功能描述、輸入輸出pad、相關(guān)回調(diào)函數(shù)等AVFilter *buffersink=avfilter_get_by_name("buffersink");AVFilterInOut *outputs = avfilter_inout_alloc();//AVFilterInOut對應(yīng)buffer和buffersink這兩個(gè)首尾端的filter的輸入輸出AVFilterInOut *inputs = avfilter_inout_alloc();filter_graph = avfilter_graph_alloc(); /* buffer video source: the decoded frames from the decoder will be inserted here. */ snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", ifmt_ctx->streams[0]->codec->width, ifmt_ctx->streams[0]->codec->height, ifmt_ctx->streams[0]->codec->pix_fmt, ifmt_ctx->streams[0]->time_base.num, ifmt_ctx->streams[0]->time_base.den, ifmt_ctx->streams[0]->codec->sample_aspect_ratio.num, ifmt_ctx->streams[0]->codec->sample_aspect_ratio.den); ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph);//根據(jù)指定的Filter,這里就是buffer,構(gòu)造對應(yīng)的初始化參數(shù)args,二者結(jié)合即可創(chuàng)建Filter的示例,并放入filter_graph中 if (ret < 0) { printf("Cannot create buffer source\n"); return ret; } /* buffer video sink: to terminate the filter chain. */ ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { printf("Cannot create buffer sink\n"); return ret; } /* Endpoints for the filter graph. */ outputs->name = av_strdup("in");//對應(yīng)buffer這個(gè)filter的output outputs->filter_ctx = buffersrc_ctx; outputs->pad_idx = 0; outputs->next = NULL; inputs->name = av_strdup("out");//對應(yīng)buffersink這個(gè)filter的input inputs->filter_ctx = buffersink_ctx; inputs->pad_idx = 0; inputs->next = NULL; if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, NULL)) < 0)//filter_descr是一個(gè)filter命令,例如"overlay=iw:ih",該函數(shù)可以解析這個(gè)命令,然后自動(dòng)完成FilterGraph中各個(gè)Filter之間的聯(lián)接 return ret; if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)//檢查當(dāng)前所構(gòu)造的FilterGraph的完整性與可用性 return ret; avfilter_inout_free(&inputs); avfilter_inout_free(&outputs);
上面介紹的是FilterGraph的構(gòu)造方法之一,即根據(jù)filter命令使用avfilter_graph_parse_ptr自動(dòng)進(jìn)行構(gòu)造,當(dāng)然也可以由我們自己將各個(gè)filter一一聯(lián)接起來,如下,這里假設(shè)我們已經(jīng)有了buffersrc_ctx、 buffersink_ctx和一個(gè)filter_ctx
// connect inputs and outputs if (err >= 0) err = avfilter_link(buffersrc_ctx, 0, filter_ctx, 0); if (err >= 0) err = avfilter_link(filter_ctx, 0, buffersink_ctx, 0); if (err < 0) { av_log(NULL, AV_LOG_ERROR, "error connecting filters\n"); return err; } err = avfilter_graph_config(filter_graph, NULL); if (err < 0) { av_log(NULL, AV_LOG_ERROR, "error configuring the filter graph\n"); return err; } return 0;
不過在filter較多的情況下,還是直接使用avfilter_graph_parse_ptr比較方便
在構(gòu)造好FilterGraph之后,就可以開始使用了,使用流程也很簡單,先將一個(gè)AVFrame幀推入FIlterGraph中,在將處理后的AVFrame從FilterGraph中拉出來即可,這里以上一篇文章的編解碼核心模塊的代碼為例看一下實(shí)現(xiàn)過程。可以看到,是將解碼得到的pFrame推入filter_graph,將處理后的數(shù)據(jù)寫入picref中,他也是一個(gè)AVFrame。需要注意的是,這里依然要將picref轉(zhuǎn)換為YUV420的幀之后再進(jìn)行編碼,一方面是因?yàn)槲覀冞@里用的是攝像頭數(shù)據(jù),是RGB格式的,另一方面,諸如curves這樣的filter是在RGB空間進(jìn)行處理的,最后得到的也是對應(yīng)像素格式的幀,所以需要進(jìn)行轉(zhuǎn)換。其他部分基本和原來一樣。
//start decode and encode int64_t start_time=av_gettime(); while (av_read_frame(ifmt_ctx, dec_pkt) >= 0){ if (exit_thread) break; av_log(NULL, AV_LOG_DEBUG, "Going to reencode the frame\n"); pframe = av_frame_alloc(); if (!pframe) { ret = AVERROR(ENOMEM); return -1; } //av_packet_rescale_ts(dec_pkt, ifmt_ctx->streams[dec_pkt->stream_index]->time_base, // ifmt_ctx->streams[dec_pkt->stream_index]->codec->time_base); ret = avcodec_decode_video2(ifmt_ctx->streams[dec_pkt->stream_index]->codec, pframe, &dec_got_frame, dec_pkt); if (ret < 0) { av_frame_free(&pframe); av_log(NULL, AV_LOG_ERROR, "Decoding failed\n"); break; } if (dec_got_frame){#if USEFILTER pframe->pts = av_frame_get_best_effort_timestamp(pframe); if (filter_change) apply_filters(ifmt_ctx); filter_change = 0; /* push the decoded frame into the filtergraph */ if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0) { printf("Error while feeding the filtergraph\n"); break; } picref = av_frame_alloc(); /* pull filtered pictures from the filtergraph */ while (1) { ret = av_buffersink_get_frame_flags(buffersink_ctx, picref, 0); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; if (ret < 0) return ret; if (picref) { img_convert_ctx = sws_getContext(picref->width, picref->height, (AVPixelFormat)picref->format, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); sws_scale(img_convert_ctx, (const uint8_t* const*)picref->data, picref->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); sws_freeContext(img_convert_ctx); pFrameYUV->width = picref->width; pFrameYUV->height = picref->height; pFrameYUV->format = PIX_FMT_YUV420P;#else sws_scale(img_convert_ctx, (const uint8_t* const*)pframe->data, pframe->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); pFrameYUV->width = pframe->width; pFrameYUV->height = pframe->height; pFrameYUV->format = PIX_FMT_YUV420P;#endif enc_pkt.data = NULL; enc_pkt.size = 0; av_init_packet(&enc_pkt); ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame); av_frame_free(&pframe); if (enc_got_frame == 1){ //printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size); framecnt++; enc_pkt.stream_index = video_st->index; //Write PTS AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;//{ 1, 1000 }; AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;// { 50, 2 }; AVRational time_base_q = { 1, AV_TIME_BASE }; //Duration between 2 frames (us) int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1)); //內(nèi)部時(shí)間戳 //Parameters //enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base)); enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base); enc_pkt.dts = enc_pkt.pts; enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base); //(double)(calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base)); enc_pkt.pos = -1; //Delay int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q); int64_t now_time = av_gettime() - start_time; if (pts_time > now_time) av_usleep(pts_time - now_time); ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt); av_free_packet(&enc_pkt); }#if USEFILTER av_frame_unref(picref); } } #endif } else { av_frame_free(&pframe); } av_free_packet(dec_pkt); }
這里我們還可以實(shí)現(xiàn)一個(gè)按下不同的數(shù)字鍵就添加不同的濾鏡的功能,如下
可以看到,首先寫好一些要用的filter命令,然后在多線程的回調(diào)函數(shù)里監(jiān)視用戶的按鍵情況,根據(jù)不同的按鍵使用對應(yīng)的filter命令初始化filter_graph,這里“null”也是一個(gè)filter命令,用于將輸入視頻原樣輸出
#if USEFILTERint filter_change = 1;const char *filter_descr="null";const char *filter_mirror = "crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right]; [left]pad=iw*2[a];[a][right]overlay=w";const char *filter_watermark = "movie=test.jpg[wm];[in][wm]overlay=5:5[out]";const char *filter_negate = "negate[out]";const char *filter_edge = "edgedetect[out]";const char *filter_split4 = "scale=iw/2:ih/2[in_tmp];[in_tmp]split=4[in_1][in_2][in_3][in_4];[in_1]pad=iw*2:ih*2[a];[a][in_2]overlay=w[b];[b][in_3]overlay=0:h[d];[d][in_4]overlay=w:h[out]";const char *filter_vintage = "curves=vintage";typedef enum{ FILTER_NULL =48, FILTER_MIRROR , FILTER_WATERMATK, FILTER_NEGATE, FILTER_EDGE, FILTER_SPLIT4, FILTER_VINTAGE}FILTERS;AVFilterContext *buffersink_ctx;AVFilterContext *buffersrc_ctx;AVFilterGraph *filter_graph;AVFilter *buffersrc;AVFilter *buffersink;AVFrame* picref;#endifDWORD WINAPI MyThreadFunction(LPVOID lpParam){ #if USEFILTER int ch = getchar(); while (ch != '\n') { switch (ch){ case FILTER_NULL: { printf("\nnow using null filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = "null"; getchar(); ch = getchar(); break; } case FILTER_MIRROR: { printf("\nnow using mirror filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_mirror; getchar(); ch = getchar(); break; } case FILTER_WATERMATK: { printf("\nnow using watermark filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_watermark; getchar(); ch = getchar(); break; } case FILTER_NEGATE: { printf("\nnow using negate filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_negate; getchar(); ch = getchar(); break; } case FILTER_EDGE: { printf("\nnow using edge filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_edge; getchar(); ch = getchar(); break; } case FILTER_SPLIT4: { printf("\nnow using split4 filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_split4; getchar(); ch = getchar(); break; } case FILTER_VINTAGE: { printf("\nnow using vintage filter\nPress other numbers for other filters:"); filter_change = 1; filter_descr = filter_vintage; getchar(); ch = getchar(); break; } default: { getchar(); ch = getchar(); break; } }#else while ((getchar())!='\n') { ;#endif } exit_thread = 1; return 0;}
除了在API層面調(diào)用AVFilter之外,還可以自己寫一個(gè)FIlter,實(shí)現(xiàn)自己想要的功能,比如前面用到的反相功能,就是用255減去原來的像素?cái)?shù)據(jù)值實(shí)現(xiàn)的,在后面的文章中會(huì)專門介紹如何自己編寫一個(gè)Filter。
此外,針對多輸入的Filter使用也是一個(gè)比較難的點(diǎn),期待大家的交流。
本項(xiàng)目源代碼
下載地址。
github地址:https://github.com/zhanghuicuc/ffmpeg_camera_streamer