怎么在视频上叠加字幕和Logo--技术实现1

2023-05-16

 这篇文章我给大家讲解的这种字幕叠加和Logo叠加方法是在渲染视频的时候“画“上去的,其实是通过某种API将OSD和Logo绘制到显卡缓存,然后提交缓存到屏幕。我们知道渲染视频有几种常用的API:GDI,DirectDraw,D3D,OpenGL,SDL,其中SDL库是对前面几种API在不同平台上的封装,是一个大集合。我给大家演示的例子是针对Windows平台的,一般在Windows平台上我们会用DirectDraw或D3D绘制图像(不建议用GDI,因为效率低),而我写的这个叠加字幕和Logo的例子是基于DirectDraw绘图的,DirectDraw API用起来比较简单,并且效率也不错。

用本文介绍的DirectDraw API来绘制字幕和Logo(位图),实现起来并不复杂。因为DirectDraw创建的表面能通过GetDC方法获得一个DC,那么你可以往这个DC上画任何东西,就像用GDI在Windows窗口上绘图一样简单。基于DirectDraw实现,叠加字幕和叠加Logo没有太大区别,因为对显卡来说,两者都是一个图层,你可以往显卡缓存的某个坐标上输出一段字符,也可以画一个位图,只是输出字符和绘制位图调用的API有点不同。因为DirectDraw绘图对字幕叠加和位图叠加都是同样原理,所以没有必要对这两种对象的实现方式作一一介绍,下面提到的字幕叠加的实现方式跟位图叠加方式基本是一样的。

为了更清晰的给大家讲解这种方法的实现思路,我写了一个例子来演示叠加字幕和位图的效果。先亮一下这个Demo的界面:

     

这个例子具有的功能:播放视频,显示OSD文字,显示OSD图标,关闭OSD。打开视频从“文件”菜单选择一个视频文件的路径,然后按“开始播放”,视频就开始渲染到界面的窗口中。点击“编辑菜单”可以对OSD的打开关闭进行控制。

这个例子对视频的分离和解码用到FFmpeg库,而视频的显示和字幕和图标(Logo)的显示则用的是DirectDraw。

下面讲一下代码实现的流程。

因为用到FFmpeg,需要在预编译头文件引用FFmpeg的头文件和静态库:

#ifdef __cplusplus
extern "C" {
#endif 

#ifdef HAVE_AV_CONFIG_H
#undef HAVE_AV_CONFIG_H
#endif

#include "./include/libavcodec/avcodec.h"
#include "./include/libavutil/mathematics.h"
#include "./include/libavutil/avutil.h"
#include "./include/libswscale/swscale.h"
#include "./include/libavutil/fifo.h"
#include "./include/libavformat/avformat.h"
#include "./include/libavutil/opt.h"
#include "./include/libavutil/error.h"
#include "./include/libswresample/swresample.h"
#include "./include/libavutil/audio_fifo.h"
#include "./include/libavutil/time.h"
#ifdef __cplusplus
}
#endif

#pragma comment( lib, "avcodec.lib")
#pragma comment( lib, "avutil.lib")
#pragma comment( lib, "avformat.lib")
#pragma comment(lib,  "swresample.lib")
#pragma comment(lib,  "swscale.lib" )

还有要初始化FFmpeg库:

 avcodec_register_all();
 av_register_all();

程序中用到几个重要的类:

FileStreamReadTask: 这个类封装了FFmpeg对媒体文件的音视频分离,和视频解码的解码操作。并提供回调函数将解码出来的帧传到外部调用者。

CDXDrawPainter(基类是CVideoPlayer): 这个类负责渲染视频图像,以及在视频上叠加字幕和图标,支持叠加多个OSD区域,可以动态打开或关闭某个OSD图层显示。

CMainFrame: 这个主窗口界面内,负责管理各种对象,处理界面上某些Windows消息事件,菜单和按钮的响应都放在这个类里去处理。

CVideoDisplayWnd: 视频显示窗口,目前只是在OnPaint函数里用黑色画刷填充窗口背景,没有其他操作。

其中,在CMainFrame类里面定义了如下几个对象:

    FileStreamReadTask   m_FileStreamTask; //解码视频
	CDXDrawPainter       m_Painter; //用DirectDraw绘制
	CVideoDisplayWnd     m_wndView; //视频窗口类

在界面菜单上选择文件后,然后点击“开始播放”,会调用到CMainFrame的一个方法OnStartStream:

LRESULT  CMainFrame:: OnStartStream(WPARAM wParam, LPARAM lParam)
{
	if(strlen(m_szFilePath) == 0)
	{
		return 1;
	}

	int nRet = m_FileStreamTask.OpenMediaFile(m_szFilePath);
	if(nRet != 0)
	{
		MessageBox(_T("打开文件失败"), _T("提示"), MB_OK|MB_ICONERROR);
		return -1;
	}
	
	long cx, cy;
	cx = cy = 0;
	m_FileStreamTask.GetVideoSize(cx, cy); //获取视频的分辨率
	if(cx > 0 && cy > 0)
	{
		m_Painter.SetVideoWindow(m_wndView.GetSafeHwnd()); //设置视频预览窗口
		m_Painter.SetRenderSurfaceType(SURFACE_TYPE_YV12);
		m_Painter.SetSourceSize(CSize(cx, cy));
		m_Painter.Open(); //打开渲染器
	}

	m_FileStreamTask.SetVideoCaptureCB(VideoCaptureCallback);

	m_FileStreamTask.StartReadFile();

	StartTime = timeGetTime();

	m_frmCount = 0;
	m_nFPS = 0;
	m_bCapture = TRUE;

	return 0;
}

而“停止播放”触发的另外一个方法是OnStopStream,代码如下:

LRESULT  CMainFrame:: OnStopStream(WPARAM wParam, LPARAM lParam)
{
	m_FileStreamTask.StopReadFile();
    //m_Painter.Close();
	m_Painter.Stop();
	m_wndView.Invalidate(); //刷新视频窗口

	//TRACE("播放用时:%d 秒\n", (timeGetTime() - StartTime)/1000);
    m_bCapture = FALSE;
	StartTime = 0;

	return 0;
} 

在OnStartStream方法中,我们设置了视频解码后回调图像的回调函数:m_FileStreamTask.SetVideoCaptureCB(VideoCaptureCallback);

其中VideoCaptureCallback回调函数的实现代码是:

//视频图像回调
LRESULT CALLBACK VideoCaptureCallback(AVStream * input_st, enum PixelFormat pix_fmt, AVFrame *pframe, INT64 lTimeStamp)
{
	if(gpMainFrame->IsPreview())
	{
	   gpMainFrame->m_Painter.PlayAVFrame(input_st, pframe);
	}

	return 0;
}

m_Painter是CDXDrawPainter类的对象,负责渲染视频和叠加字幕,图标。回调函数中调用了m_Painter对象的一个方法PlayAVFrame来转换输入的图像格式,然后在函数内显示视频和OSD。
接着,看一下CDXDrawPainter类的声明,它继承与CVideoPlayer类。

class CDXDrawPainter : public CVideoPlayer
{
private:
	//VIDEOINFOHEADER *	mVideoInfo;
	HWND			mVideoWindow;
	HDC				mWindowDC;
	RECT			mTargetRect;
	RECT			mSourceRect;
	BOOL			mNeedStretch;
  
   	struct SwsContext  *img_convert_ctx;
	BYTE   *       m_pYUVDataCache; //YV12的临时缓冲区,Y,U,V平面依次排列,将非YV12的图像格式转换后存储到这里

public:
	CDXDrawPainter();
	~CDXDrawPainter();

	void SetVideoWindow(HWND inWindow);
	//BOOL SetInputFormat(BYTE * inFormat, long inLength);

	BOOL Open(void);
	BOOL Stop(void);
	BOOL Play(BYTE * inData, DWORD inLength, ULONG inSampleTime, int inputFormat); //传入的图像支持YUY2,YV12,RGB24和RGB32, 指针inData指向图像的首地址
	BOOL PlayAVFrame(AVStream * st, AVFrame * picture); //显示FFmpeg的AVFrame的图像数据, 传入的AVFrame Format可以是RGB或YUV格式,为了统一处理,函数内会将所有格式转换为YV12

	void  SetSourceSize(CSize size);
	BOOL  GetSourceSize(CSize & size);

};

Play方法将一个视频图像传入,并带上时间戳,图像格式等信息。PlayAVFrame方法的作用跟Play函数一样,只是它的参数有点不一样,它接受的不是一个连续内存的图像地址,而是一个FFmpeg的AVFrame结构指针,指向的结构携带了图像数据地址信息,还有解码出来的图像帧信息。Play和PlayAVFrame两个函数都是负责渲染视频和绘制OSD,但后者对图像数据多一些转换操作(下文会提到)。为了控制OSD显示外观,我们必须设置OSD显示属性(比如OSD的文字,显示坐标,显示颜色等),这就需要调用CDXDrawPainter基类中的几个方法,基类是CVideoPlayer。

让我们看看CVideoPlayer类里面有哪些重要的方法:

void    SetRenderSurfaceType(RenderSurfaceType type) { m_SurfaceType = type; }

// Initialization
int     Init( HWND hWnd , BOOL bUseYUV, int width, int height); 
void    Uninit();

void    SetPlayRect(RECT r);

// Rendering
BOOL	RenderFrame(BYTE* frame,int w,int h, int inputFmt);

//设置OSD接口
BOOL    SetOsdText(int nIndex, CString strText, COLORREF TextColor, RECT & OsdRect);
BOOL    SetOsdBitmap(int nIndex, HBITMAP hBitmap, RECT & OsdRect);
void    DisableOsd(int nIndex);

下面是这些方法的说明:

1.  SetRenderSurfaceType:设置Directdraw表面创建的类型,目前支持YUY2,YV12.

2. Init:  创建和初始化DirectDraw表面,传入视频窗口的句柄和图像大小,并且设置是否用Overlay模式,bUserYUV参数为TRUE表示使用Overlay模式,但是Overlay模式经常使用不了,并且有很多限制。建议该参数赋值为FALSE。

3. Unit:  销毁DirectDraw表面。

4. SetPlayRect:设置视频在窗口中的显示区域。

5. RenderFrame: 显示图像和叠加OSD。父类CDXDrawPainter的成员Play和PlayAVFrame 都会在内部调用这个函数。参数意义:inputFmt是传入的图像格式,取值为:0--YV12, 1--YUY2, 2--RGB24, 3--RGB32。

6.  SetOsdText: 设置OSD文字的相关属性,包括Index,字符内容,文字颜色,和显示坐标。

7.  SetOsdBitmap: 设置叠加的OSD位图属性,包括Index,传入位图句柄,显示位置。

8.  DisableOsd:关闭OSD。

OSD的信息用一个结构体类型--OSDPARAM表示,OSDPARAM定义如下:

typedef struct _osdparam
{
	BOOL     bEnable;
	int      nIndex;
	char     szText[128];
	LOGFONT  mLogFont;
	COLORREF clrColor;
	RECT     rcPosition;
	HBITMAP  hWatermark;
}OSDPARAM;

我们定义了一个OSD数组: OSDPARAM       m_OsdInfo[MAX_OSD_NUM];

所有OSD信息都存到这个数组里,Index是OSD的一个索引,每个OSD是数组的一个成员,可自定义最大的叠加的OSD个数。

下面看看CVideoPlayer::Init怎么创建DiectDraw表面的。

int CVideoPlayer::Init( HWND hWnd , BOOL bUseYUV, int width, int height )
{

	HRESULT hr;

	m_hWnd = hWnd;

	m_bOverlay = bUseYUV;

	// DDraw stuff begins here
	if( FAILED( hr = DirectDrawCreateEx( NULL, (VOID**)&m_pDD,
		IID_IDirectDraw7, NULL ) ) )
		return FALSE;

	// Set cooperative level
	hr = m_pDD->SetCooperativeLevel( hWnd, DDSCL_NORMAL );
	if( FAILED(hr) )
		return FALSE;

	// Create the primary surface
	DDSURFACEDESC2 ddsd;


	ZeroMemory( &ddsd, sizeof( ddsd ) );
	ddsd.dwSize         = sizeof( ddsd );
	ddsd.dwFlags        = DDSD_CAPS;
	ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

	// Create the primary surface.
	if( FAILED( m_pDD->CreateSurface( &ddsd, &m_pddsFrontBuffer, NULL ) ) )
		return FALSE;


	if(m_bOverlay)  // use Overlay
	{

		DDCAPS caps;
		ZeroMemory(&caps, sizeof(DDCAPS));
		caps.dwSize = sizeof(DDCAPS);

		if (m_pDD->GetCaps(&caps, NULL)==DD_OK)
		{
			if (caps.dwCaps & DDCAPS_OVERLAY)
			{

				ASSERT(m_SurfaceType == SURFACE_TYPE_YUY2 || m_SurfaceType == SURFACE_TYPE_YV12);

				ZeroMemory(&ddsd, sizeof(DDSURFACEDESC2));
				ddsd.dwBackBufferCount = 0;
				ddsd.dwSize = sizeof(DDSURFACEDESC2);
				ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT;
				ddsd.ddsCaps.dwCaps = DDSCAPS_OVERLAY /*| DDSCAPS_VIDEOMEMORY*/;
				ddsd.dwWidth = width;
				ddsd.dwHeight = height;


				DDPIXELFORMAT ddPixelFormat;
				ZeroMemory(&ddPixelFormat, sizeof(DDPIXELFORMAT));

				ddPixelFormat.dwSize = sizeof(DDPIXELFORMAT);
				ddPixelFormat.dwFlags = DDPF_FOURCC;

				if(m_SurfaceType == SURFACE_TYPE_YUY2)
				{
					ddPixelFormat.dwFourCC = MAKEFOURCC('Y','U','Y','2'); //YUY2
					ddPixelFormat.dwYUVBitCount = 16;
					m_nBitCount = 16;
				}
				else if(m_SurfaceType == SURFACE_TYPE_YV12)
				{
					ddPixelFormat.dwFourCC = MAKEFOURCC('Y','V','1','2'); //YV12
					ddPixelFormat.dwYUVBitCount = 12;
					m_nBitCount = 12;
				}
				else
				{
					return FALSE;
				}

				memcpy(&(ddsd.ddpfPixelFormat), &ddPixelFormat, sizeof(DDPIXELFORMAT));

				// Create overlay surface
				hr = m_pDD->CreateSurface(&ddsd, &m_pddsOverlay, NULL);
				if(FAILED(hr))
				{
					TRACE("CreateSurface failed!! \n");
					return FALSE;
				}

				hr=m_pDD->CreateClipper(0, &m_pddClipper, NULL);
				hr=m_pddClipper->SetHWnd(0, m_hWnd);
				hr=m_pddsFrontBuffer->SetClipper(m_pddClipper);

			} 
		} 
		else
		{
			ASSERT(0);
			return FALSE;
		}

	} 

	else  //None Overlay
	{

		ZeroMemory( &ddsd, sizeof(ddsd) );
		ddsd.dwSize         = sizeof(ddsd);
		ddsd.dwFlags        = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT;
		ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN /*| DDSCAPS_VIDEOMEMORY*/;
		ddsd.dwWidth		= width;
		ddsd.dwHeight		= height;


		// DDPIXELFORMAT pft  = {sizeof(DDPIXELFORMAT), DDPF_RGB, 0, 16,  0x0000001F, 0x000007E0, 0x0000F800, 0};

		if(m_SurfaceType == SURFACE_TYPE_YUY2)
		{
			DDPIXELFORMAT pft  =   { sizeof(DDPIXELFORMAT), DDPF_FOURCC, MAKEFOURCC('Y', 'U', 'Y', '2'), 0, 0, 0, 0, 0};
			memcpy(&ddsd.ddpfPixelFormat, &pft, sizeof(DDPIXELFORMAT)); 
			m_nBitCount = 16;
		}
		else if(m_SurfaceType == SURFACE_TYPE_YV12)
		{
			DDPIXELFORMAT pft  =   { sizeof(DDPIXELFORMAT), DDPF_FOURCC, MAKEFOURCC('Y', 'V', '1', '2'), 0, 0, 0, 0, 0};
			memcpy(&ddsd.ddpfPixelFormat, &pft, sizeof(DDPIXELFORMAT)); 
			m_nBitCount = 12;
		}
		//else if(m_SurfaceType == SURFACE_TYPE_RGB24)
		//{
		//	DDPIXELFORMAT pft = {sizeof(DDPIXELFORMAT), DDPF_RGB, 0, 24,  0x000000FF, 0x0000FF00, 0x00FF0000, 0}; 
		//	memcpy(&ddsd.ddpfPixelFormat, &pft, sizeof(DDPIXELFORMAT)); 
		//	m_nBitCount = 24;
		//}
		//else if(m_SurfaceType == SURFACE_TYPE_RGB32)
		//{
		//	DDPIXELFORMAT pft = {sizeof(DDPIXELFORMAT), DDPF_RGB, 0, 32,  0x000000FF, 0x0000FF00, 0x00FF0000, 0}; 
		//	memcpy(&ddsd.ddpfPixelFormat, &pft, sizeof(DDPIXELFORMAT)); 
		//	m_nBitCount = 32;
		//}
		else
		{
			ASSERT(0);
			return FALSE;
		}


		// Create  surface
		if(FAILED(m_pDD->CreateSurface(&ddsd, &m_pddsBack, NULL)))
		{
			TRACE("CreateSurface failed!!! \n");
			return FALSE;
		}

		/*create clipper for non overlay mode*/
		hr=m_pDD->CreateClipper(0, &m_pddClipper, NULL);
		hr=m_pddClipper->SetHWnd(0, m_hWnd);
		hr=m_pddsFrontBuffer->SetClipper(m_pddClipper);

	}


	
	//OSD表面
	ZeroMemory(&ddsd, sizeof(ddsd));
	ddsd.dwSize = sizeof(ddsd);
	ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH ;
	ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
	ddsd.dwWidth = width;
	ddsd.dwHeight = height;

	//HRESULT hr;
	hr = m_pDD->CreateSurface(&ddsd, &m_pddsOSD, NULL);
	if ( hr != DD_OK)
	{
		CString sError;
		sError.Format("Create m_pOsdSurface failed:%d, %.8x\n", GetLastError(), hr);
		OutputDebugString(sError);
		return FALSE;
	}
	
	return TRUE;
}

Init函数调用的时候会创建显示视频的后备缓冲区(Back Buffer)和前缓冲区(Front Buffer),DirectDraw Suface其实就是后备缓冲区。视频图像和OSD、位图在RenderFrame调用时会先拷贝到后备缓冲区,然后再送到前缓冲区(通过Blt操作)显示。对于YUY2和YV12的图像会直接拷贝到后备缓冲区(前提是图像格式和DDraw Surface的格式对应),其他格式的会转为YV12再拷贝到缓冲区(SurfaceType为YV12)。

强调一下:CVideoPlayer接受的YUY2和YV12图像是各个分量紧密存储到一起的连续内存块,而FFmpeg解码出来的是一个AVFrame,默认是YUV420P格式,其中Y,U,V是分开三个平面存储的,那么传给CVideoPlayer类必须把这三个分离的平面数据拷贝到另外一个内存块,使得三个平面的内存连续。所以PlayAVFrame需要对AVFrame作一些数据转换操作。

下面是PlayAVFrame函数的代码:

//显示FFmpeg的AVFrame的图像数据, 传入的pFrame可以是RGB或YUV格式,如果不是YV12,则会进行转换
BOOL CDXDrawPainter::PlayAVFrame(AVStream * st, AVFrame * picture)
{

	if(m_pYUVDataCache == NULL)
	{
		int nYUVSize = st->codec->width * st->codec->height*2; 
		m_pYUVDataCache = new BYTE[nYUVSize];	
		memset(m_pYUVDataCache, 0, nYUVSize);
	}

	if(st->codec->pix_fmt != PIX_FMT_YUV420P && st->codec->pix_fmt != PIX_FMT_YUVJ420P )
	{
		if(img_convert_ctx == NULL)
		{
			img_convert_ctx = sws_getContext(st->codec->width, st->codec->height,
				st->codec->pix_fmt,
				st->codec->width, st->codec->height,
				PIX_FMT_YUV420P,
				SWS_BICUBIC, NULL, NULL, NULL);

			if (img_convert_ctx == NULL)
			{
				TRACE("sws_getContext() failed \n");
				return FALSE;		
			}
		}

		int nWidth  = st->codec->width;
		int nHeight = st->codec->height;

		AVPicture	dstbuf;

		dstbuf.data[0] = (uint8_t*)m_pYUVDataCache;
		dstbuf.data[2] = (uint8_t*)m_pYUVDataCache + nWidth*nHeight;
		dstbuf.data[1] = (uint8_t*)m_pYUVDataCache + nWidth*nHeight + nWidth*nHeight / 4;
		dstbuf.linesize[0] = nWidth;
		dstbuf.linesize[1] = nWidth / 2;
		dstbuf.linesize[2] = nWidth / 2;
		dstbuf.linesize[3] = 0;

		//转成YV12格式
		sws_scale(img_convert_ctx, picture->data, picture->linesize,  0, st->codec->height, dstbuf.data, dstbuf.linesize);

		RenderFrame(m_pYUVDataCache, nWidth, nHeight, 0);

	}
	else //YUV420P
	{
		int Y_PLANE_SIZE, U2_PLANE_SIZE, V2_PLANE_SIZE;

		int nWidth  = st->codec->width;
		int nHeight = st->codec->height;

		Y_PLANE_SIZE  = nWidth * nHeight;
		U2_PLANE_SIZE = nWidth * nHeight/4;			 
		V2_PLANE_SIZE = nWidth * nHeight/4;

		uint8_t  *dest_y = picture->data[0];
		uint8_t  *dest_v = picture->data[1];
		uint8_t  *dest_u = picture->data[2];

		//将Y、u、v三个分离的平面数据拷贝到YV12的连续内存区
		if(m_pYUVDataCache != NULL)
		{
			if(picture->linesize[0] == st->codec->width)
			{
				memcpy(m_pYUVDataCache, dest_y, Y_PLANE_SIZE);
				memcpy(m_pYUVDataCache + Y_PLANE_SIZE, dest_u, U2_PLANE_SIZE);
				memcpy(m_pYUVDataCache + Y_PLANE_SIZE + U2_PLANE_SIZE, dest_v, V2_PLANE_SIZE);
			}
			else
			{

				for (int i=0; i<nHeight; i++)
				{
					memcpy((byte*)m_pYUVDataCache + i*nWidth, 
						picture->data[0] + i*picture->linesize[0],
						nWidth);
				}
				int half_h = nHeight/2;
				int half_x = nWidth/2;

				for (int i=0; i<half_h; i++)
				{
					memcpy((byte*)m_pYUVDataCache + Y_PLANE_SIZE +i*nWidth/2, 
						picture->data[2] + i*picture->linesize[2], 
						half_x);

				}
				for (int i=0; i<half_h; i++)
				{
					memcpy((byte*)m_pYUVDataCache + Y_PLANE_SIZE + U2_PLANE_SIZE + i*nWidth/2, 
						picture->data[1] + i*picture->linesize[1], 
						half_x);
				}	

			}
		}


		RenderFrame(m_pYUVDataCache, nWidth, nHeight, 0);
	}

	return TRUE;
}

在上面代码中,PlayAVFrame函数内部调用了RenderFrame函数,而RenderFrame才是负责视频显示和OSD的绘制的核心函数。下面是RenderFrame函数的代码,代码有点长,但是大家浏览一下,大概能明白其中的流程。

//函数名:  RenderFrame
//函数作用:渲染图像
//参数意义:
//   frame -- 传入的图像Buffer地址
//   w, h --  图像宽高
//   inputFmt -- 图像像素格式, 0--YV12, 1--YUY2, 2--RGB24, 3--RGB32
//-----------------------------------------------------------------------------
BOOL CVideoPlayer::RenderFrame(BYTE* frame,int w,int h, int inputFmt)
{
	HRESULT hr;
	RECT prect,orect;
	int ok_decode=1;
	DDSURFACEDESC2 ddsd;

	ZeroMemory( &ddsd, sizeof( ddsd ) );
	ddsd.dwSize         = sizeof( ddsd );

	if(m_pddsFrontBuffer->IsLost()==DDERR_SURFACELOST)
		m_pddsFrontBuffer->Restore();

	if(m_bOverlay)
	{
		//do something
       //我把Overlay处理的代码给省略了(在源文件中是有的),因为跟非Overlay的处理基本一样。
	}

	else //None Overlay
	{

		if(m_pddsBack->IsLost()==DDERR_SURFACELOST)
			m_pddsBack->Restore();

		if(m_pddsBack->Lock( NULL, &ddsd, DDLOCK_SURFACEMEMORYPTR|DDLOCK_WAIT, NULL)!=DD_OK)
			return FALSE;


		if(inputFmt == 1 && m_SurfaceType == SURFACE_TYPE_YUY2)
		{
			ASSERT(m_nBitCount == 16);

			int nBytesPerLine = ddsd.dwWidth*m_nBitCount/8;

			BYTE * srcAddr  = frame;
			BYTE * destAddr = (BYTE*)ddsd.lpSurface; 

			for(int i = 0; i<ddsd.dwHeight; i++)
			{
				memcpy(destAddr, srcAddr, nBytesPerLine);
				destAddr += ddsd.lPitch;
				srcAddr  += nBytesPerLine;
			}
		}

		else if(inputFmt == 0 && m_SurfaceType == SURFACE_TYPE_YV12)
		{

			BYTE * srcAddr  = frame;
			BYTE * destAddr = (BYTE*)ddsd.lpSurface; 

			//Note: 下面注释的代码对于YV12的处理方式是错误的
			// int  nSrcLineSize  = w *m_nBitCount/8;
			// int  nDestLineSize = ddsd.lPitch;

			// for(int i = 0; i<ddsd.dwHeight; i++)
			// {
			//	 memcpy(destAddr, srcAddr, nSrcLineSize);
			//	 destAddr += nDestLineSize;
			//	 srcAddr  += nSrcLineSize;
			//}

			long        y_size, u_size;
			long        y_buf_size, u_buf_size;
			BYTE        *p_DestBuf, *p_SrcBuf;
			int         buf_height, buf_width, i;

			buf_width  = ddsd.lPitch;
			buf_height = ddsd.dwHeight;

			y_size = w * h;
			u_size = y_size >> 2;

			y_buf_size = buf_width * buf_height;
			u_buf_size = y_buf_size >> 2;


			p_SrcBuf  = srcAddr;
			p_DestBuf = destAddr;

			for(i = 0; i < h; i++){
				memcpy(p_DestBuf, p_SrcBuf, w);
				p_SrcBuf += w;
				p_DestBuf += buf_width;
			}

			p_DestBuf = destAddr + y_buf_size;
			p_SrcBuf = srcAddr + y_size;
			for(i = 0; i < h/2; i++){
				memcpy(p_DestBuf, p_SrcBuf, w/2);
				p_SrcBuf  += w/2;
				p_DestBuf += buf_width/2;
			}

			p_DestBuf = destAddr + y_buf_size + u_buf_size;
			p_SrcBuf  = srcAddr  + y_size     + u_size;
			for(i = 0; i < h/2; i++){
				memcpy(p_DestBuf, p_SrcBuf, w/2);
				p_SrcBuf  += w/2;
				p_DestBuf += buf_width/2;
			}

		}
		else if((inputFmt == 2 || inputFmt == 3) && m_SurfaceType == SURFACE_TYPE_YV12) //RGB24/RGB32
		{
			//将RGB转为YV12

			long        y_size, u_size;
			long        y_buf_size, u_buf_size;
			BYTE        *p_DestBuf, *p_SrcBuf;
			int         buf_height, buf_width, i;

			buf_width  = ddsd.lPitch;
			buf_height = ddsd.dwHeight;

			y_size = w * h;
			u_size = y_size >> 2;

			y_buf_size = buf_width * buf_height;
			u_buf_size = y_buf_size >> 2;


			if(m_picture_buf == NULL)
				m_picture_buf = (BYTE*)malloc(y_size * 3/2); //为YV12的图像缓冲区分配内存

			img_convert_ctx = sws_getContext(w, h, (inputFmt == 2) ? PIX_FMT_RGB24 :PIX_FMT_RGB32, 
				w, h, PIX_FMT_YUV420P, SWS_POINT, NULL, NULL, NULL);

			if (img_convert_ctx == NULL)
			{
				OutputDebugString("sws_getContext() failed \n");	
			}


			BOOL bRevertPicture = TRUE; //解决RGB图像倒置显示的问题

			if(bRevertPicture)
			{
				uint8_t  *dest_y = m_picture_buf;
				uint8_t  *dest_u = dest_y  + y_size;
				uint8_t  *dest_v = dest_y  + y_size + u_size;

				int nSrcLineSize = w * (inputFmt == 2 ? 3 : 4);

				uint8_t *rgb_src[3] = { frame + (h - 1)*nSrcLineSize, NULL, NULL};
				int rgb_stride[3]   = { -nSrcLineSize, 0, 0};

				uint8_t *dest[3]= { dest_y, dest_u, dest_v };
				int dest_stride[3] = { w, w/2, w/2 };

				sws_scale(img_convert_ctx, rgb_src, rgb_stride, 0, h, dest, dest_stride);
			}
			else
			{
				uint8_t  *dest_y = m_picture_buf;
				uint8_t  *dest_u = dest_y  + y_size;
				uint8_t  *dest_v = dest_y  + y_size + u_size;


				int nSrcLineSize = w * (inputFmt == 2 ? 3 : 4);

				uint8_t *rgb_src[3] = { frame, NULL, NULL};
				int rgb_stride[3]   = { nSrcLineSize, 0, 0};

				uint8_t *dest[3]= { dest_y, dest_u, dest_v };
				int dest_stride[3] = { w, w/2, w/2 };


				sws_scale(img_convert_ctx, rgb_src, rgb_stride, 0, h, dest, dest_stride);
			}

		
			BYTE * srcAddr  = m_picture_buf;
			BYTE * destAddr = (BYTE*)ddsd.lpSurface; 

		   	p_SrcBuf  = srcAddr;
			p_DestBuf = destAddr;

			for(i = 0; i < h; i++){
				memcpy(p_DestBuf, p_SrcBuf, w);
				p_SrcBuf += w;
				p_DestBuf += buf_width;
			}

			p_DestBuf = destAddr + y_buf_size;
			p_SrcBuf = srcAddr + y_size;
			for(i = 0; i < h/2; i++){
				memcpy(p_DestBuf, p_SrcBuf, w/2);
				p_SrcBuf  += w/2;
				p_DestBuf += buf_width/2;
			}

			p_DestBuf = destAddr + y_buf_size + u_buf_size;
			p_SrcBuf  = srcAddr  + y_size     + u_size;
			for(i = 0; i < h/2; i++){
				memcpy(p_DestBuf, p_SrcBuf, w/2);
				p_SrcBuf  += w/2;
				p_DestBuf += buf_width/2;
			}


		}

		if (m_pddsBack->Unlock(NULL)!=DD_OK)
			return FALSE;


		AdjustRectangle(&orect,&prect,w,h);


#if 1
		CAutoLock lock(&m_csOSDLock);

		if(m_pddsOSD != NULL)
		{
			hr = m_pddsOSD->Blt(&orect, m_pddsBack, &orect, DDBLT_WAIT, NULL);
			if (hr != DD_OK)
			{
				hr = m_pddsFrontBuffer->Blt(&prect, m_pddsBack, &orect, DDBLT_WAIT, NULL);
			}
			else
			{
				HDC hDC = NULL;
				hr = m_pddsOSD->GetDC(&hDC);
				if ((hr == DD_OK)&&(hDC != NULL))
				{
					for(int nIndex = 0; nIndex < MAX_OSD_NUM; nIndex++)
					{
						if(!m_OsdInfo[nIndex].bEnable)
							continue;

						if(strlen(m_OsdInfo[nIndex].szText) > 0)
						{
							SetBkMode(hDC, TRANSPARENT);
							SetTextColor(hDC, m_OsdInfo[nIndex].clrColor);

							DrawText(hDC, m_OsdInfo[nIndex].szText, strlen(m_OsdInfo[nIndex].szText), &m_OsdInfo[nIndex].rcPosition, DT_LEFT|DT_VCENTER);
						}

						else if(m_OsdInfo[nIndex].hWatermark != NULL)
						{
							m_hMemDC = ::CreateCompatibleDC(hDC);
							::SelectObject(m_hMemDC, m_OsdInfo[nIndex].hWatermark);

							BITMAP bmpinfo;
			                ::GetObject(m_OsdInfo[nIndex].hWatermark,  sizeof(BITMAP), &bmpinfo);

							int nLeft =  m_OsdInfo[nIndex].rcPosition.left;
							int nTop = m_OsdInfo[nIndex].rcPosition.top;
							int nDestWidth = m_OsdInfo[nIndex].rcPosition.right - m_OsdInfo[nIndex].rcPosition.left;
							int nDestHeight = m_OsdInfo[nIndex].rcPosition.bottom - m_OsdInfo[nIndex].rcPosition.top;
							int nSrcWidth = bmpinfo.bmWidth;
							int nSrcHeight = bmpinfo.bmHeight;

							::TransparentBlt(hDC, nLeft, nTop, nDestWidth, nDestHeight, m_hMemDC, 0, 0, nSrcWidth, nSrcHeight, RGB(255,255,255));
						
							DeleteDC(m_hMemDC);
						}

					}

					m_pddsOSD->ReleaseDC(hDC);

					m_pddsFrontBuffer->Blt(&prect, m_pddsOSD, &orect, DDBLT_WAIT, NULL);

				}
			}
		}
#else
	   hr = m_pddsFrontBuffer->Blt(&prect, m_pddsBack, &orect, DDBLT_WAIT, NULL);
#endif

		//m_pddsFrontBuffer->BltFast(prect.left,prect.top,m_pddsBack,&orect,0);
		//hr = m_pddsFrontBuffer->Blt(&prect,m_pddsBack, &orect, DDBLT_WAIT, 0);

	}

	return TRUE;
}

例子代码下载地址:https://download.csdn.net/download/zhoubotong2012/11855592

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

怎么在视频上叠加字幕和Logo--技术实现1 的相关文章

  • 算法:皇后问题

    问题 国际象棋中的皇后 xff0c 可以横向 纵向 斜向移动 如何在一个NXN的棋盘上放置N个皇后 xff0c 使得任意两个皇后都不在同一条横线 竖线 斜线方向上 xff1f 举个栗子 xff0c 下图的绿色格子是一个皇后在棋盘上的 封锁范
  • OpenCV+Python二维码条形码识别

    先上源码 xff0c github地址 xff1a https github com DerrickRose25 Opencv QRcode recognition 环境 xff1a Pycharm Python3 7 在pycharm里安
  • Onvif协议:实现Probe命令来进行设备发现(discover)

    在onvif协议对接中 xff0c 首先要明确服务器和客户端的身份 服务器 xff1a 通常是你要对接的其他厂家的数字摄像头 xff08 IPC xff09 客户端 xff1a 通常是对接的ipc的设备程序 xff0c 安防业内多称 xff
  • Linux C/C++编程:Udp组播(多播)

    Udp多播简介 概叙 单播用于两个主机之间单对单的通信广播用于一个主机对整个局域网上所有主机上的数据通信单播和广播是两个极端 xff0c 要么对一个主机进行通信 xff0c 要么对整个局域网的主机进行通信实际情况下 xff0c 经常需要对一
  • cmake:同一目录下多个源文件

    此文为 xff1a 轻松入门cmake系列教程前文为 xff1a cmake xff1a Hello cmake 接下来进入稍微复杂的例子 xff1a 在同一个目录下有多个源文件 第一个实验 实践 在之前的目录下添加2个文件 xff0c t
  • cmake:string

    字符串操作 概要 Search span class token operator and span Replace span class token function string span span class token punctu
  • 性能:你知道并发用户数应该怎么算吗

    我们知道 xff0c 一个性能测试中 xff0c 往往会有各种各样的指标 xff0c 比如TPS RPS QPS HPS CPM等 我们在实际工作的时候 xff0c 应该对这些概念有统一的认识 建议使用TPS作为关键的性能指标 另外 xff
  • C/C++面试:手写智能指针类

    shared ptr原理 shared ptr实际上是对裸指针进行了一层封装 xff0c 成员变量除了裸指针之外 xff0c 还有一个引用计数 xff0c 它记录裸指针被引用的次数 xff08 有多少个shared ptr指向这同一个裸指针

随机推荐

  • golang:http.request

    request 表示由服务器接收或由客户端发送的HTTP请求 xff0c 例如客户端 client 在发送各种请求时 xff0c 需要先新建一个请求对象 xff0c 然后调用一些请求的方法开始自定义一些配置 xff0c 服务端监听到该请求便
  • git:smartgit

    下载安装smartgit 下载 xff1a https www syntevo com smartgit download 终端下操作 xff1a 执行命令 xff1a tar xvf smartgit tar gz 执行命令 xff1a
  • golang:如何在proto3中用上golang对应的interface{}类型

    首先 xff0c 我希望所有golang中用于http请求响应的结构 xff0c 都使用proto3来定义 麻烦的是 xff0c 有的情况下某个字段的类型可能是动态的 xff0c 对应的JSON类型可能是number string bool
  • 如何计算icmp校验和

    前几天看到大佬写的一篇关于icmp远控后门文章 xff0c 对icmp协议充满了激情 xff0c 通过查阅资料了解相关所需的知识 xff0c 实现整个程序首先要了解imcp包请求与回复 xff0c 在整个请求中最先就涉及到icmp包的构造
  • 作为一个自动化本科生到底应该学些什么(讲讲个人经历和感受)

    2019 6 26日深夜 晚上在床上准备休息 xff0c 一个同跟我聊天说自己大学荒废了 xff0c 突然有很多感想想写一写 我觉得自己大学也是得过且过地过着的 xff0c 不过虽然充满艰辛和不满意 xff0c 但还是挺充实的 先自我介绍一
  • 嵌入式项目实战——基于QT的视频监控系统设计(一)

    嵌入式项目实战 基于QT的视频监控系统设计 xff08 一 xff09 这个五一因为疫情 xff0c 只能待在家里 xff0c 想了想不如将我之前做的一个小的嵌入式的练习项目分享出来 xff0c 供入门嵌入式的同学们学习 基于QT的视频监控
  • 嵌入式项目实战——基于QT的视频监控系统设计(三)

    嵌入式项目实战 基于QT的视频监控系统设计 xff08 三 xff09 进入到五一假期第三天 xff0c 继续我们的项目 本来五一假期还是想好好休息一下的 xff0c 因为最近学习的状态不太好 xff0c 刷题都没有思路了 xff0c 但是
  • 嵌入式开发板RS485协议串口编程——角度传感器数据读取

    嵌入式开发板RS485协议串口编程 倾角传感器数据读取 之前分享过一篇嵌入式操作系统开发板中的串口编程 光敏电阻数据读取 xff0c 是基于TTL协议的串口编程 xff0c 本节主要讲述基于RS485协议的串口编程 xff0c 掌握了这两种
  • 嵌入式开发板CAN通信编程——伺服电机驱动

    嵌入式开发板CAN通信编程 伺服电机驱动 在实际的嵌入式项目开发过程中 xff0c 若不涉及上位机与开发板的通信传输数据 xff0c 那最关键的无非就是两个内容 xff0c 读取传感器的数据并处理 xff0c 驱动硬件设备工作 传感器数据的
  • 嵌入式字符设备驱动——ULN2003步进电机驱动程序实现

    嵌入式字符设备驱动 ULN2003步进电机驱动程序实现 之前分享了字符设备驱动程序的实现 hello驱动 xff0c 是不涉及硬件操作的 xff0c 我说过要给大家分享一篇涉及硬件操作的字符设备驱动程序的实现 xff0c 今天周末休息 xf
  • Linux多进程间通信——共享内存实现

    Linux多进程间通信 共享内存实现 又到了每周分享时刻 xff0c 这周我要分享的是关于Linux中进程间通信问题 xff0c 这对于底层程序的实现至关重要 xff0c 进程间通信方式主要包括管程 共享内存 消息传递 套接字这几种方式 x
  • 嵌入式开发板RS485多节点串口编程——关节力矩传感器数据读取

    嵌入式开发板RS485多节点串口编程 关节力矩传感器数据读取 最近学业繁忙 xff0c 主要是准备找工作 xff0c 有一段时间没分享了 xff0c 今天给大家分享一下我最近利用TI AM4376开发板RS485串口读取两个关节力矩传感器的
  • Linux多进程间通信——管道通信实现

    Linux多进程间通信 管道通信实现 之前分享了linux多进程间通信的两种方法 xff0c 套接字和共享内存通信 今天来分享一下另外一种多进程通信方法 管道 管道分为有名管道和无名管道 无名管道用于有亲缘关系之间的进程 xff0c 即父子
  • Linux多进程间通信——消息传递实现

    Linux多进程间通信 消息队列实现 之前已经分享了共享内存 管道 套接字来实现多进程的通信 xff0c 下面再介绍一下消息队列 xff0c 后面我还会再介绍最后一个多进程的通信方式 xff0c 通过信号来实现 xff0c 这样多进程通信的
  • RS485总线究竟能挂接多少个设备?

    N年前做门禁系统上位机软件开发的时候突击培训过串口通信编程基础 后来在我的脑海里一直认为RS485总线能且只能挂接256个设备 xff08 因为地址是1byte xff0c 取值范围也就0 255 xff09 后来经过几个项目的了解 xff
  • Python Raw Socket使用示例(发送TCP SYN数据包)

    python view plain copy import sys import time import socket import struct import random def SendPacketData Buffer 61 Non
  • mysql-server 依赖 mysql-server-5.5 解决方案

    问题 ubuntu14 04 3安装mysql时报错 xff1a sudo apt get install mysql server mysql client 正在读取软件包列表 完成 正在分析软件包的依赖关系树 正在读取状态信息 完成 有
  • 生成aruco码方法

    有两种方法得到想要的aruco码 xff1a 1 直接通过网址得到 http chev me arucogen xff08 不过只有四个格式 xff09 网页截图为 xff1a 2 通过运行C 43 43 代码得到 利用C 43 43 生成
  • 超详细c语言简化tcp通信接口(多线程实现一个服务端处理多个客户端服务)

    超详细c语言tcp通信接口 1 可下载源码 xff08 客户端 服务端通信 xff09 2 说明3 接口代码4 客户端通信main client demo c5 服务端通信main server demo c 1 可下载源码 xff08 客
  • 怎么在视频上叠加字幕和Logo--技术实现1

    这篇文章我给大家讲解的这种字幕叠加和Logo叠加方法是在渲染视频的时候 画 上去的 xff0c 其实是通过某种API将OSD和Logo绘制到显卡缓存 xff0c 然后提交缓存到屏幕 我们知道渲染视频有几种常用的API xff1a GDI x