ffmpeg超詳細綜合教程(二)——爲直播流添加濾鏡
https://blog.csdn.net/wh8_2011/article/details/73506128
2017年06月20日 18:52:16 -鳴人- 閱讀數:6873
在上一篇文章中,講解了如何利用ffmpeg實現攝像頭直播,本文將在此基礎上,實現一個可以選擇各種視頻濾鏡的攝像頭直播示例。本文包含以下內容
1、AVFilter的基本介紹
2、如何利用ffmpeg命令行工具實現各種視頻濾鏡
3、如何利用libavfilter編程實現在攝像頭直播流中加入各類不同濾鏡的功能
具有較強的綜合性。
AVFilter的基本介紹
AVFilter的功能十分強大,可以實現對多媒體數據的各種處理,包括時間線編輯、視音頻特效濾鏡的添加或信號處理,還可以實現多路媒體流的合併或疊加,其豐富程度令人歎爲觀止。這裏主要以視頻濾鏡爲例進行介紹。使用AVFilter可以爲單路視頻添加單個或多個濾鏡,也可以爲多路視頻分別添加不同的濾鏡並且在最後將多路視頻合併爲一路視頻,AVFilter爲實現這些功能定義了以下幾個概念:
Filter:代表單個filter
FilterPad:代表一個filter的輸入或輸出端口,每個filter都可以有多個輸入和多個輸出,只有輸出pad的filter稱爲source,只有輸入pad的filter稱爲sink
FilterLink:若一個filter的輸出pad和另一個filter的輸入pad名字相同,即認爲兩個filter之間建立了link
FilterChain:代表一串相互連接的filters,除了source和sink外,要求每個filter的輸入輸出pad都有對應的輸出和輸入pad
FilterGraph:FilterChain的集合
基本和DirectShow類似,也與視頻後期調色軟件中的節點等概念類似。具體來看,以下面的命令爲例
[in]split[main][tmp];[tmp]crop=iw:ih/2,vflip[flip];[main][flip]overlay=0:H/2[out]
在該命令中,輸入流[in]首先被分[split]爲兩個流[main]和[tmp],然後[tmp]流經過了剪切[crop]和翻轉[vflip]兩個濾鏡後變爲[flip],這時我們將[flip]疊加[overlay]到最開始的[main]上形成最後的輸出流[out],最後呈現出的是鏡像的效果。下圖清晰地表示了以上過程
我們可以認爲圖中每一個節點就是一個Filter,每一個方括號所代表的就是FilterPad,可以看到split的輸出pad中有一個叫tmp的,而crop的輸入pad中也有一個tmp,由此在二者之間建立了link,當然input和output代表的就是source和sink,此外,圖中有三條FilterChain,第一條由input和split組成,第二條由crop和vflip組成,第三條由overlay和output組成,整張圖即是一個擁有三個FilterChain的FilterGraph。
上面的圖是人工畫出來的,也可以在代碼中調用avfilter_graph_dump函數自動將FilterGraph畫出來,如下
可以看到,多出來了一個scale濾鏡,這是由ffmpeg自動添加的用於格式轉換的濾鏡。
在FFmpeg命令行工具中使用AVFilter
在命令行中使用AVFilter需要遵循專門的語法,簡單來說,就是每個Filter之間以逗號分隔,每個Filter自己的屬性之間以冒號分隔,屬性和Filter以等號相連,多個Filter組成一個FilterChain,每個FilterChain之間以分號相隔。AVFilter在命令行工具中對應的是-vf或-af或-filter_complex,前兩個分別對應於單路輸入的視頻濾鏡和音頻濾鏡,最後的filter_complex則對應於有多路輸入的情況。除了在FFMpeg命令行工具中使用外,在FFplay中同樣也可以使用AVFilter。其他一些關於單雙引號、轉義符號等更詳細的語法參考Filter
Documentation
下面舉幾個例子
1、疊加水印
ffmpeg -i test.flv -vf movie=test.jpg[wm];[in][wm]overlay=5:5[out] out.flv
將test.jpg作爲水印疊加到test.flv的座標爲(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、調整曲線
ffmpeg -i test.flv -vf curves=vintage out.flv
類似Photoshop裏面的曲線調整,這裏的vintage是ffmpeg自帶的預設,實現復古畫風,還可以直接加載其他的Photoshop預設文件並在其基礎上加以調整,如下
ffmpeg -i test.flv -vf curves=psfile='test.acv':green='0.45/0.53' out.flv
其中的acv預設文件實現的是加強對比度,再次基礎上調整綠色的顯示效果,以上兩個命令的最終效果如下
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
正如前面所說的,當有多個輸入時,需要使用filter_complex,效果如下
通過以上幾個例子,基本可以明白在命令行中使用AVFilter時需要遵循的語法。
使用libavfilter編程爲直播流添加濾鏡
要使用libavfilter,首先要註冊相關組件
avfilter_register_all();
首先需要構造出一個完整可用的FilterGraph,需要用到輸入流的解碼參數,參見上一篇文章,如下
-
AVFilterContext *buffersink_ctx;//看名字好像AVFilterContext是什麼很厲害的東西,但其實只要認爲它是AVFilter的一個實例就OK了
-
AVFilterContext *buffersrc_ctx;
-
AVFilterGraph *filter_graph;
-
AVFilter *buffersrc=avfilter_get_by_name("buffer");//Filter的具體定義,只要是libavfilter中已註冊的filter,就可以直接通過查詢filter名字的方法獲得其具體定義,所謂定義即filter的名稱、功能描述、輸入輸出pad、相關回調函數等
-
AVFilter *buffersink=avfilter_get_by_name("buffersink");
-
AVFilterInOut *outputs = avfilter_inout_alloc();//AVFilterInOut對應buffer和buffersink這兩個首尾端的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);//根據指定的Filter,這裏就是buffer,構造對應的初始化參數args,二者結合即可創建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");//對應buffer這個filter的output
-
outputs->filter_ctx = buffersrc_ctx;
-
outputs->pad_idx = 0;
-
outputs->next = NULL;
-
inputs->name = av_strdup("out");//對應buffersink這個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是一個filter命令,例如"overlay=iw:ih",該函數可以解析這個命令,然後自動完成FilterGraph中各個Filter之間的聯接
-
return ret;
-
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)//檢查當前所構造的FilterGraph的完整性與可用性
-
return ret;
-
avfilter_inout_free(&inputs);
-
avfilter_inout_free(&outputs);
上面介紹的是FilterGraph的構造方法之一,即根據filter命令使用avfilter_graph_parse_ptr自動進行構造,當然也可以由我們自己將各個filter一一聯接起來,如下,這裏假設我們已經有了buffersrc_ctx、 buffersink_ctx和一個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比較方便
在構造好FilterGraph之後,就可以開始使用了,使用流程也很簡單,先將一個AVFrame幀推入FIlterGraph中,在將處理後的AVFrame從FilterGraph中拉出來即可,這裏以上一篇文章的編解碼核心模塊的代碼爲例看一下實現過程。可以看到,是將解碼得到的pFrame推入filter_graph,將處理後的數據寫入picref中,他也是一個AVFrame。需要注意的是,這裏依然要將picref轉換爲YUV420的幀之後再進行編碼,一方面是因爲我們這裏用的是攝像頭數據,是RGB格式的,另一方面,諸如curves這樣的filter是在RGB空間進行處理的,最後得到的也是對應像素格式的幀,所以需要進行轉換。其他部分基本和原來一樣。
-
//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)); //內部時間戳
-
//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);
-
}
這裏我們還可以實現一個按下不同的數字鍵就添加不同的濾鏡的功能,如下
可以看到,首先寫好一些要用的filter命令,然後在多線程的回調函數裏監視用戶的按鍵情況,根據不同的按鍵使用對應的filter命令初始化filter_graph,這裏“null”也是一個filter命令,用於將輸入視頻原樣輸出
-
#if USEFILTER
-
int 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;
-
#endif
-
DWORD 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層面調用AVFilter之外,還可以自己寫一個FIlter,實現自己想要的功能,比如前面用到的反相功能,就是用255減去原來的像素數據值實現的,在後面的文章中會專門介紹如何自己編寫一個Filter。
此外,針對多輸入的Filter使用也是一個比較難的點,期待大家的交流。
本項目源代碼下載地址。
github地址:https://github.com/zhanghuicuc/ffmpeg_camera_streamer