在上一周,我用QT+FFMPEG显示电脑桌面,总算可以实时显示视频画面。在此基础上,我想为视频添加水印信息。网上的教程大多以命令行为主,少数有利用的代码的方式添加水印,而且都只能对已有文件进行操作,我还是期望能实时操作并显示添加水印的视频。
AVFilter
相关结构体
根据网友和官方提供的示例代码,得知FFMPEG提供了AVFilter库可以满足需求,AVFilter库其中包含了音频和视频滤镜,目前我只是用到了简单的视频滤镜。AVFilter中有重要的结构体需要我们了解,对理解代码有一定帮助。
结构体 | 说明 |
---|---|
AVFilterGrahp | 管理整个过滤过程 |
AVFilter | 内部实现的滤镜类 |
AVFilterContext | 滤镜上下文,用于维护滤波相关信息的实例 |
AVFilterInOut | 滤镜链的输入输出链接列表, |
AVFilterLink | 滤镜链,链接两个相邻两个AVFilterContext。在实际应用时,可能涉及到多个滤镜协作,一个滤镜的输出可以作为另一个滤镜的输入。 |
AVFilterPad | 滤镜的输入和输出称为pad,一个AVFilter可以有多个输入和多个输出端口,AVFilterLink两端分别为前一个滤镜的输入pad和后一个滤镜的输入pad |
buffersrc | 特殊滤镜,作为整个滤波过程的入口,通过调用av_buffersrc_add_frame可以将需要滤波的帧放入滤波过程。因此在创建该滤镜时,需要提供额外的信息指示输入帧的格式,例如time_base,图像的宽高音频采样格式等 |
buffersink | 和buffersrc对应,作为滤波过程出口的特殊滤镜。通过调用av_buffersink_get_frame提取滤波后的帧 |
调用代码
在之前的测试代码基础上,我又添加了滤镜相关的变量和函数,其实这部分可以作为全局变量,但是为了风格统一,还是将它们放在的类里面,具体定义和实现如下:
//滤镜相关
AVFilterGraph * filter_graph = nullptr;
AVFilterContext *buffersink_ctx = nullptr;;
AVFilterContext *buffersrc_ctx = nullptr;;
int CWorkThread::InitFilter(AVCodecContext *codecContext)
{
char args[512];
int ret = 0;
//创建滤镜容器
filter_graph = avfilter_graph_alloc();
//获取源滤镜和Sink滤镜
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
//初始化滤镜输入和输出
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
//滤镜的描述
unsigned int myt= 10+time(NULL);
std::string stime = std::to_string(myt);
std::string filters_descr = "drawtext=fontfile=arial.ttf:fontcolor=red:fontsize=50:x=0:y=0:text='%{pts\\:localtime\\:"+stime+"\\:%Y-%m-%d\\ \\%H\\\\\\:%M\\\\\\:%S}'";
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
do {
if (!outputs || !inputs || !filter_graph)
{
ret = AVERROR(ENOMEM);
break;
}
//初始化数据帧的格式
sprintf_s(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
codecContext->width, codecContext->height, codecContext->pix_fmt,
codecContext->time_base.num, codecContext->time_base.den,
codecContext->sample_aspect_ratio.num, codecContext->sample_aspect_ratio.den);
//输入数据缓存
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
//输出数据缓存
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
NULL, NULL, filter_graph);
//设置输出的pix_fmt
ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
AV_PIX_FMT_YUV420P, AV_OPT_SEARCH_CHILDREN);
//设置滤镜的端点
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;
//初始化滤镜
if ((ret = avfilter_graph_parse_ptr(filter_graph, filters_descr.c_str(),
&inputs, &outputs, NULL)) < 0)
{
break;
}
//滤镜生效
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
{
break;
}
} while (false);
//释放对应的输入输出
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
滤镜初始化过程解析
InitFilter函数是整个滤波过程最关键的,传入的AVCodecContext参数只是为了告诉滤镜输入帧的格式,实际可以根据需要对该参数进行修改。InitFileter只需要在过滤前调用一次就好,然后就可以在获取源视频帧的位置调用av_buffersrc_add_frame_flags函数,将需要滤波的帧添加到滤镜中。然后再调用av_buffersink_get_frame获取滤波后的帧。下图是InitFilter对应的流程
InitFilter流程图
疑问记录
在设置滤镜端点的时候,outputs->name设置的是“in”,而inputs->name设置的是“out”,第一次看到时觉得有些奇怪,甚至怀疑是写错了。后来看到一个解释:outputs对应的是源buffersrc,作为滤波过程的起点,它必定是输出端。同理inputs对应buffersink,作为滤波的终点,它必定是只有输入端。
运行效果
下面是运行的截图,可以看到画面左上角有一个当前时间的时间戳