前面两篇文章详细讲解了怎么叠加字幕和Logo,但是这两篇的例子主要是针对Windows平台的,用到大量Windows API,一些非Windows程序员想要移植到其他平台(如Linux、Android)可能还要费一番功夫。要在其他平台进行叠加字幕和Logo有什么比较通用的方案呢?其实FFmpeg已经集成了一个加水印滤镜功能,用跨平台的FFmpeg能够帮助我们轻松实现该功能。
废话少说,先看看加水印滤镜怎么用。
首先要调用avfilter_register_all() 注册所有AVFilter。
接着,定义几个跟加水印滤镜相关的变量:
AVFilterContext * buffersink_ctx = NULL;
AVFilterContext * buffersrc_ctx = NULL;
AVFilterGraph * filter_graph = NULL;
BOOL g_bInitFilterOK = FALSE;
CCritSec g_FilterLock;
FFmpeg初始化加水印滤镜的例子代码如下:
static int init_filters(const char *filters_descr, AVCodecContext *pCodecCtx)
{
CAutoLock lock(&g_FilterLock);
if(filter_graph != NULL)
return 1;
if(pCodecCtx->pix_fmt != PIX_FMT_YUV420P) //检查输入图像像素格式
return 2;
char args[512];
int ret;
AVFilter *buffersrc = avfilter_get_by_name("buffer");
AVFilter *buffersink = avfilter_get_by_name("ffbuffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
enum PixelFormat pix_fmts[] = { PIX_FMT_YUV420P, PIX_FMT_NONE };
AVBufferSinkParams *buffersink_params;
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",
pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->time_base.num, pCodecCtx->time_base.den,
pCodecCtx->sample_aspect_ratio.num, pCodecCtx->sample_aspect_ratio.den);
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
TRACE("Cannot create buffer source\n");
return ret;
}
/* buffer video sink: to terminate the filter chain. */
buffersink_params = av_buffersink_params_alloc();
buffersink_params->pixel_fmts = pix_fmts;
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
NULL, buffersink_params, filter_graph);
av_free(buffersink_params);
if (ret < 0) {
TRACE("Cannot create buffer sink\n");
return ret;
}
/* Endpoints for the filter graph. */
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,
&inputs, &outputs, NULL)) < 0)
return ret;
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
return ret;
g_bInitFilterOK = TRUE;
return 0;
}
上面代码主要用到的API如下:
avfilter_graph_alloc():为FilterGraph分配内存。
avfilter_graph_create_filter():创建并向FilterGraph中添加一个Filter。
avfilter_graph_parse_ptr():将一串通过字符串描述的Graph添加到FilterGraph中。
avfilter_graph_config():检查FilterGraph的配置。
av_buffersrc_add_frame():向FilterGraph中加入一个AVFrame。
av_buffersink_get_frame():从FilterGraph中取出一个AVFrame。
init_filters函数需要传入一个字符串,这个字符串是描述要加水印的属性的,包括:水印图片的路径,水印显示坐标等。水印支持PNG图片文件作为输入,即支持背景透明。这个字符串的格式如下:
movie=logo.png,scale=60:30[watermask];[in] [watermask] overlay=30:10 [out]
参数说明:
logo.png: 添加的水印图片;
scale:水印大小,水印长度*水印的高度;
overlay:水印的位置,距离原视频左侧的距离:距离原视频上侧的距离;mainW主视频宽度, mainH主视频高度,overlayW水印宽度,overlayH水印高度
左上角overlay参数为 overlay=0:0
右上角为 overlay= main_w-overlay_w:0
右下角为 overlay= main_w-overlay_w:main_h-overlay_h
左下角为 overlay=0: main_h-overlay_h
FFmpeg水印滤镜支持的参数见下面表格:
参数 |
参数 |
说明 |
overlay |
main_w |
视频单帧图像宽度 |
main_h |
视频单帧图像高度 |
|
overlay_w |
水印图片的宽度 |
|
overlay_h |
水印图片的高度 |
|
-vf |
设置video过滤器,视频旋转,缩放,水印等处理 |
|
af |
设置audio过滤器 |
关于更多的参数可以参考ffmpeg官网filter的描述:https://ffmpeg.org/ffmpeg-filters.html
下面是在程序中调用init_filters的代码:
CString strFilterLogoDesc;
if(m_bIsOSDText)
strFilterLogoDesc = "movie=logo_text.png[watermark];[in][watermark]overlay=10:10[out]"; //movie=后面的参数是Logo图标的文件名,overlay=后面的是坐标,OSD坐标位置暂时固定为(10, 10)
else
strFilterLogoDesc = "movie=logo.png[watermark];[in][watermark]overlay=10:10[out]";
int nRet = init_filters(strFilterLogoDesc, input_st->codec);
这里要说明一下,Logo文件的路径默认是跟执行文件同一个目录的,如果要用绝对路径指定Logo的路径,则可能会失败。我在Windows平台上测试过,Logo文件用绝对路径的话,init_filters将会返回-2(不知在Linux上是否也有这个问题)。后来找到一个解决办法,Logo.png的路径不用绝对路径,但要设置当前目录的路径:调用系统API设置SetCurrentDirectory(path),将目标目录路径指定为Logo文件所在的目录。
从上面调用代码可以看到,对于叠加字幕和叠加图标都需要传入一个水印PNG文件,对于文字怎么生成一个图片文件呢?在Windows平台,我们可以用Windows GDI 函数在内存中生成一个位图,然后打印上字符,并保存为PNG图片(额!还是得用Windows API,不是说跨平台吗?这个只是作者本人的技术倾向,对Windows API比较熟悉,其实不直接用系统API也可以,一些跨平台库如SDL、QT也有类似在的内存中打印文字和输出图形的功能)。下面是从一段文字转为一个带Alpha通道的位图的代码:
//创建一个显示OSD文字的位图,位图带透明背景
static BOOL CreateOsdTextBitmap(const char * szText, LOGFONT * lplf, CImage & image)
{
BOOL pass = FALSE;
CSize csTextSize;
HDC memDC = CreateCompatibleDC(NULL);
HFONT hFont = ::CreateFontIndirect(lplf);
ASSERT(hFont != NULL);
::SelectObject(memDC, hFont);
GetTextExtentPoint32(memDC, szText, lstrlen(szText), &csTextSize);
int cx = csTextSize.cx + 12;
int cy = csTextSize.cy + 8;
HBITMAP membmp = CreateCompatibleBitmap(memDC, cx, cy);
HBITMAP oldbmp = (HBITMAP) SelectObject(memDC, membmp);
SetBkMode(memDC, TRANSPARENT);
::SetBkColor(memDC, RGB(0, 0, 0)); //背景色
SetTextColor(memDC, RGB(0xFF, 0, 0));
CRect OsdRect(6, 4, cx-6, cy-4);
DrawText(memDC, szText, lstrlen(szText), &OsdRect, DT_CENTER);
#if 1
if(!image.Create(cx, cy, 32, 0x01)) //创建带Alpha通道的32位位图
#else
if(!image.Create(cx, cy, 32))
#endif
{
pass = FALSE;
goto end_osd_bitmap_func;
}
pass = TRUE;
HDC hImgDC = image.GetDC();
BitBlt(hImgDC, 0, 0, cx, cy, memDC, 0, 0, SRCCOPY);
image.ReleaseDC();
#if 1
if(image.GetBPP() == 32)
{
//将OSD背景的部分设置为透明
int image_cx = image.GetWidth();
int image_cy = image.GetHeight();
long lPitch = image.GetPitch();
BYTE * image_data;
if(lPitch < 0)
{
image_data = (BYTE *)image.GetBits()+(image.GetPitch()*(image.GetHeight()-1));
}
else
{
image_data = (BYTE *)image.GetBits();
}
BYTE * pImage = NULL;
for(int y = 0; y < image_cy; y++)
{
pImage = image_data + abs(lPitch) * y;
for(int x = 0; x < image_cx; x++)
{
if(pImage[0] == 0 && pImage[1] == 0 && pImage[2] == 0) //RGB等于背景色
{
pImage[3] = 0; //透明
}
else
{
pImage[3] = 0xff;
}
pImage += 4;
}
}
}
#endif
end_osd_bitmap_func:
::DeleteObject(hFont);
SelectObject(memDC, oldbmp);
DeleteObject(membmp);
DeleteDC(memDC);
return pass;
}
对于字幕,我们保存的图片名是logo_text.png;而对于Logo,我们加载Logo图片(logo.png)。
初始化完滤镜后,我们在视频图像解码出来之后就可以往上叠上水印了。解码出来的图像帧通过回调函数传递到应用层,这个回调函数的实现如下:
//视频图像回调
LRESULT CALLBACK VideoCaptureCallback(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
{
if(gpMainFrame->IsOverlayOSD())
{
CAutoLock lock(&g_FilterLock);
if(filter_graph == NULL)
{
CString strFilterLogoDesc;
//注意:Logo图片需要跟执行文件同一个目录,并且要调用SetCurrentDirectory设置当前目录,否则初始化Filter将会失败,返回-2
if(gpMainFrame->m_bIsOSDText)
strFilterLogoDesc = "movie=logo_text.png[watermark];[in][watermark]overlay=10:10[out]"; //movie=后面的参数是Logo图标的文件名,overlay=后面的是坐标,OSD坐标位置暂时固定为(10, 10)
else
strFilterLogoDesc = "movie=logo.png[watermark];[in][watermark]overlay=10:10[out]";
::SetCurrentDirectory(GetAppDir());
int nRet = init_filters(strFilterLogoDesc, input_st->codec);
if(nRet != 0)
{
TRACE("Error: init_filters failed!! nRet = %d\n", nRet);
goto end_filter;
}
}
if(!g_bInitFilterOK)
goto end_filter;
AVFilterBufferRef *picref;
pframe->pts = av_frame_get_best_effort_timestamp(pframe);
if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0)
{
TRACE( "Error while feeding the filtergraph\n");
goto end_filter;
}
int ret;
while (1)
{
ret = av_buffersink_get_buffer_ref(buffersink_ctx, &picref, 0);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
if (ret < 0)
break;
if (picref)
{
int y_size=picref->video->w*picref->video->h;
AVFrame outframe;
outframe.data[0] = picref->data[0];
outframe.data[1] = picref->data[1];
outframe.data[2] = picref->data[2];
outframe.data[3] = 0;
outframe.linesize[0] = picref->linesize[0];
outframe.linesize[1] = picref->linesize[1];
outframe.linesize[2] = picref->linesize[2];
outframe.linesize[3] = 0;
gpMainFrame->m_Painter.PlayAVFrame(input_st, &outframe);
avfilter_unref_bufferp(&picref);
}
}// while
return 0;
}
else
{
#if 0
if(filter_graph != NULL)
{
avfilter_graph_free(&filter_graph);
filter_graph = NULL;
}
#endif
}
end_filter:
//if(gpMainFrame->IsPreview())
{
gpMainFrame->m_Painter.PlayAVFrame(input_st, pframe);
}
return 0;
}
上述函数中, 成员变量 m_bOverlayOSD是表示当前是否正在叠加字幕或图标;变量 m_bIsOSDText 表示叠加的是字幕;根据这两个变量在界面上更新叠加对象类型,显示不同的叠加效果(目前只支持叠加一个OSD,不同类型的OSD需要切换,读者可在基础上进行扩展)。
最后说说这个水印滤镜的缺点:就是它不支持从内存冲传入水印图片,而需要读取一个磁盘上的图片文件,如果想实现动态更新的OSD效果,比如显示一个不停更新时间的OSD,则需要频繁的构建图片-》保存图片-》加载,效率肯定不好,不知道FFmpeg对这种情况有没有更好的实现方式?欢迎大家留言评论。
例子下载地址:https://download.csdn.net/download/zhoubotong2012/11855623