ffmpeg解码H.264视频数据,MFC播放视频

ffmpeg 是一个完整的视频流解决方案,开源且有良好的跨平台性,ffmpeg具有强大的多媒体数据处理能力,能够实现视频的采集,多种视频格式间转换,给视频添加水印等多种功能,已被 VLC、Mplayer 等多种开源项目所采用,本系统客户端在进行 H.264 数据解码时利用 ffmpeg 来进行实现。

                               ffmpeg库 在vs2012中的调用


(1)下载地址:http://ffmpeg.zeranoe.com/builds/ 所需文件Builds(Dev)和Builds(Shared),ffmpeg库是在网上下载的最新版本20160409版

      Builds(Dev):包含了所需要的.h头文件和.lib库文件 

      Builds(Shared):包含了所需要的dll文件

为了解决C99的兼容问题,在vs2012/vc/include中自行添加了auto_stdint.h;auto_inttype.h 如果不添加则会出现编译错误。对应的某些ffmpeg头文件 #include和#include都要相应的改为#include和#include
(2)包含文件路径,将Build(Dev)和Build(Shared)文件夹放到D:\FFMPEG中,在MFC工程中设置ffmpeg头文件位置

左侧 属性管理器->双击工程名->配置属性 -> C/C++ -> 常规 -> 附加包含目录,添加包含文件路径。

设置ffmpeg的lib文件位置 ,鼠标右键点击工程名,选择属性,然后选择 配置属性 -> 链接器 -> 常规 -> 附加库目录,添加库文件路径。

设置ffmpeg的所引用的lib文件 鼠标右键点击工程名,选择属性,然后选择 配置属性 -> 链接器 -> 输入 -> 附加依赖项,添加的文件为你下载的 Builds (Dev)中的lib 文件。avcodec.lib; avformat.lib; avutil.lib; swscale.lib; swresample.lib; avfilter.lib;swscale.lib。这些lib库在程序编译的时候没影响,但程序运行的时候需要他们。


经过测试发现由于FFMPEG版本的不同,很多函数在的使用方法都做了改变。该版本库有些函数已经声明为已否决,但使用中需要用到需要用到。ffmpeg该版本库使用过程中使用了三个声明已否决(attribute_deprecated)的函数avpicture_get_size; avpicture_fill; av_free_packet暂时未找到替代函数。若要使用这些函数,要将声明这些函数的头文件中的attribute_deprecated注释掉。


                              用ffmpeg实现H.264视频数据的解码


    我们平时看到的视频文件有许多格式,比如 avi, mkv, rmvb, mov, mp4等等,这些被称为容器(Container), 不同的容器格式规定了其中音视频数据的组织方式(也包括其他数据,比如字幕等)。容器中一般会封装有视频和音频轨,也称为视频流(stream)和音频流,播放视频文件的第一步就是根据视频文件的格式,解析(demux)出其中封装的视频流、音频流以及字幕,解析的数据读到包 (packet)中,每个包里保存的是视频帧(frame)或音频帧,然后分别对视频帧和音频帧调用相应的解码器(decoder)进行解码,使用 H.264编码的视频会相应的调用H.264解码器,解码之后得到的就是原始的图像(YUV or RGB)。接下来简略介绍使用FFMPEG解码的步骤
(1)首先定义需要用到的结构体,在本客户端中,主要用到了以下结构体。
        AVFormatContext 保存需要读入的文件的格式信息,比如流的个数以及流数据等
        AVCodecContext  保存了相应流的详细编码信息,比如视频的宽、高,编码类型等。
        AVCodec 真正的编解码器,其中有编解码需要调用的函数
        AVFrame用于保存数据帧的数据结构
        AVFrame 用于保存转换之后的帧
        SwsContext 转换器,用于将YUV420P类型的图片转换为RGB类型
        AVPacket 解析文件时会将音/视频帧读入到packet中
(2)注册解码器,并且初始化自定义的AVIOContext,目的是在主机内存中申请内存空间,并将AVFormatContext的pb指针指向它。在使用avformat_open_input()打开媒体数据的时候,就可以不指定文件的URL了,即其第2个参数为NULL,读取的数据是由read_buffer()提供,read_buffer是回调函数,需要自定义read_buffer使其在视频解码时得到对应的数据。
        av_register_all();  
        pFormatCtx = avformat_alloc_context();  
        unsigned char *aviobuffer=(unsigned char *)av_malloc(1024*15); 
        AVIOContext*avio=avio_alloc_context(aviobuffer,1024*15 ,0,NULL,read_buffer, NU L L,NULL);  
        pFormatCtx->pb=avio;  
        int read_buffer(void *opaque, uint8_t *buf, int buf_size){......}
(3)读取接收到的数据的基本信息,用于设置解码器类型。avformat_open_input函数只是读取接收到的数据头,并不会填充流信息,因此我们需要接下来调用avformat_find_stream_info获取流信息,并确定所有的流信息。根据读取到的信息设置解码器的解码类型和解码的宽高度,使用avcodec_open2初始化解码器。
      if(avformat_open_input(&pFormatCtx,NULL,NULL,NULL)!=0){......}
      if(avformat_find_stream_info(pFormatCtx,NULL)<0){......}
      videoindex=-1;  
      for(i=0; inb_streams; i++)   {.....}//在这里查找视频流
      pCodecCtx=pFormatCtx->streams[videoindex]->codec;  
      pCodec=avcodec_find_decoder(pCodecCtx->codec_id);  
      if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){......}
(4)使用av_read_frame可以将从内存中读取的一帧数据存入packet中。通过avcodec_decode_video2调用相应的解码器,将解码后的数据存入AVFrame 中。解码之后存入AVFrame中数数据是YUV420P类型的,Y、U、V分别存放在pFrame->data[0]、pFrame->data[1]、pFrame->data[2]中。主要用到的函数为:
       if(av_read_frame(pFormatCtx, packet)>=0){......}
       avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);  


                                            在MFC中实现视频的播放显示


         最终的目的是实现用MFC播放视频,但MFC中没有用于专门的视频播放控件,我们可以利用Picture Control控件实现类似功能。需要将Picture Control的类型改为Frame,通过FFMPEG库将YUV420P数据转换为RGB类型数据,创建BITMAPINFO,调用函数StretchDIBits将一帧数据显示在Picture Control控件上。


(1)客户端在MFC中创建了界面,其中将Picture Control的类型设置为Frame。通过FFMPEG库将YUV420P数据转换为RGB类型数据,由SwsContext类设置转换前和转换后额格式,调用库中的sws_scale函数可以得到RGB的纯数据。主要在使用sws_scale函数之前需要将YUV420p做180度的旋转,不然转换出来的RGB数据是上下颠倒的


 (2) 得到了RGB信息之后要将一帧直接显示在Picture Control上还需要给RGB信息加上必要的头信息,使其成为BITMAP。定义BITMAPFILEHEADER ,BITMAPINFOHEADER 并给文件信息头和位图信息头赋值。

 (3)在视频显示线程中,通过Picture Control的ID获取控件窗口的句柄、控件所在的矩形区域、控件的DC。调用函数StretchDIBits将一帧数据显示在Picture Control控件上,示例代码如下:

           CWnd *pWnd=GetDlgItem(IDC_STATIC_PIC); //获得pictrue控件窗口的句柄
           CRect rect;
           pWnd->GetClientRect(&rect); //获得pictrue控件所在的矩形区域
           CDC *pDC=pWnd->GetDC(); //获得pictrue控件的DC
           pDC->SetStretchBltMode(COLORONCOLOR);
           StretchDIBits(pDC->GetSafeHdc(),0,0,rect.Width(),rect.Height(),0,0,pCodecCtx->width,pCodecCtx->height,pRGBFrame->data[0],                                pBmpInfo,DIB_RGB_COLORS,SRCCOPY); 


部分代码:

SOCKET soc;
SOCKET control_soc;
AVFormatContext *pFormatCtx;  
int             i, videoindex;  
AVCodecContext  *pCodecCtx;  
AVCodec         *pCodec;  
AVFrame         *pFrame; 
AVFrame         *pRGBFrame;
AVFrame         *pRGBFrameSave;
struct SwsContext *pSwsCtx;
int RGBsize;
uint8_t *outBuff;
char revBuffer[1024];
bool haveConnected;
bool isMonitor;
bool isShowContinue;
const char* cstr_c(CString str)
{
	char *chr;
	int strSize=0;
	strSize=WideCharToMultiByte(CP_ACP,0,str,-1,NULL,0,NULL,NULL);
	chr=(char*)malloc(strSize+1);
	WideCharToMultiByte(CP_ACP,0,str,-1,chr,strSize,NULL,NULL);
	const char* cchr=chr;
	return cchr;
}
DWORD WINAPI ReceiveThread ( LPVOID lParam )
{
	CMFCApplication1Dlg* pDlg = (CMFCApplication1Dlg*)lParam ;
	jrtplib::RTPSession rtpsess;
	jrtplib::RTPSessionParams sessionparams;
	jrtplib::RTPUDPv4TransmissionParams transparams;
	unsigned char *recvdata;
	int status;
	unsigned char nalu_header;
	unsigned char frameData[1024*15];
	int frameDatasize=5;
	sessionparams.SetOwnTimestampUnit(1/90000);//接收视频
	sessionparams.SetAcceptOwnPackets(true);
	transparams.SetPortbase(_ttoi(pDlg->m_port.GetString()));//_ttoi(pDlg->m_port.GetString())
	status=rtpsess.Create( sessionparams, &transparams );
	if(status<0)
	printf("rtpsessipon create error!\n");
    jrtplib::RTPIPv4Address remoteAdd(ntohl(inet_addr("192.168.2.5")));
	rtpsess.SetReceiveMode(jrtplib::RTPTransmitter::ReceiveMode::AcceptSome);
	rtpsess.AddToAcceptList(remoteAdd);
	av_register_all();  
	pFrame=av_frame_alloc();
	if(pFrame == NULL) 
	pDlg->MessageBox ( L"avcodec alloc Outputframe failed!");    
	pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);  
	if (!pCodec)   
	{  
		pDlg->MessageBox(_T("creat codec error"));
	}  
	pCodecCtx = avcodec_alloc_context3(pCodec);
	pCodecCtx->time_base.num = 1;  
	pCodecCtx->frame_number = 1; //每包一个视频帧  
	pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;  
	pCodecCtx->bit_rate = 0;  
	pCodecCtx->time_base.den = 25;//帧率  
	pCodecCtx->width = m_width;//视频宽  
	pCodecCtx->height =m_height;//视频高  
	if(avcodec_open2(pCodecCtx, pCodec,NULL) <0)
	{
		pDlg->MessageBox ( L"Could not open codec.");  
	}
	pFrame=av_frame_alloc();
	AVPacket packet = {0};  
	int frameFinished = 0;//这个是随便填入数字,没什么作用  
	pRGBFrame=av_frame_alloc();
	if(pRGBFrame == NULL) 
	{  
		pDlg->MessageBox ( L"avcodec alloc RGBframe failed!");   
	}  
	RGBsize= avpicture_get_size(AV_PIX_FMT_BGR24 ,m_width, m_height);
	outBuff = (uint8_t*)av_malloc(RGBsize);  
	if( outBuff == NULL ) 
	{  
		pDlg->MessageBox (L"av malloc failed!");  
	}  
	avpicture_fill((AVPicture *)pRGBFrame, outBuff, AV_PIX_FMT_RGB24, m_width,m_height );
	pSwsCtx = sws_getContext(m_width, m_height,AV_PIX_FMT_YUV420P, m_width, m_height, AV_PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); 
	if (pSwsCtx== NULL) 
	{
		pDlg->MessageBox (L"Cannot initialize the conversion context\n");
	}
		while(1)
	{
		//rtpsess.Poll();
		rtpsess.BeginDataAccess();
		if( rtpsess.GotoFirstSourceWithData() )
		{
			do
			{
				jrtplib::RTPPacket *rtppack;
				while( ( rtppack = rtpsess.GetNextPacket() ) != NULL )
				{
					//printf("the packet length is :%d\n",rtppack->GetPacketLength());
					//printf("the sequence number is :%d\n",rtppack->GetSequenceNumber());
					recvdata = rtppack->GetPayloadData();					memcpy(&frameData[frameDatasize],&recvdata[2],rtppack->GetPayloadLength()-2);  
					frameDatasize=frameDatasize+rtppack->GetPayloadLength()-2;
					if((recvdata[1]&0x80)==0x80)//如果作为第一个包则写起始码NALU—header
					{
						char strstart[4] = {0x00,0x00,0x00,0x01};
						memcpy(frameData,&strstart[0],4);
						nalu_header=(recvdata[0]&0x80) |(recvdata[0]&0x60)|(recvdata[1]&0x1f);
						memcpy(&frameData[4],&nalu_header,1);
							}
					if((recvdata[1]&0x40)==0x40)//作为一帧数据的最后一个包,要处理的事情,一帧接收完毕的处理
					{
					packet.data = frameData;
                        packet.size = frameDatasize;//这个填入H264数据帧的大小  
                        avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);  
						if(frameFinished)//成功解码  
						{  
							//反转图像 ,否则生成的图像是上下颠倒的  
							pFrame->data[0] += pFrame->linesize[0] * (pCodecCtx->height - 1);  
							pFrame->linesize[0] *= -1;  
					    pFrame->data[1] += pFrame->linesize[1] * (pCodecCtx->height / 2 - 1);  
							pFrame->linesize[1] *= -1;  
						pFrame->data[2] += pFrame->linesize[2] * (pCodecCtx->height / 2 - 1);  
							pFrame->linesize[2] *= -1;  
							static uint8_t *p = NULL;
							p = pFrame->data[1];
							pFrame->data[1] = pFrame->data[2];
							pFrame->data[2] = p;
						    sws_scale(pSwsCtx, pFrame->data,pFrame->linesize, 0,pCodecCtx->height,pRGBFrame->data, pRGBFrame->linesize); 
                             //把转换出来的RGB图像加上bitmap头,显示在picture control上 
					         pDlg->SaveAsBMPandShow(0,NULL);
						}
						frameDatasize=5;
					}
					rtpsess.DeletePacket( rtppack );
				}
			}while( rtpsess.GotoNextSourceWithData() );
		}
		rtpsess.EndDataAccess();
	}
	rtpsess.BYEDestroy( jrtplib::RTPTime( 10, 0 ), 0, 0 );
	return 0;
}
void CMFCApplication1Dlg::ConnectToServer(int port)
{
	UpdateData(true);
	if(port==0)
	{
		soc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(soc == INVALID_SOCKET)
		{
			WSACleanup();
			this->MessageBox ( L"创建socket失败!" ) ;
			return;
		}
		sockaddr_in  serveaddress;
		serveaddress.sin_family = AF_INET;
		serveaddress.sin_port = htons(3333);		
         serveaddress.sin_addr.S_un.S_addr = inet_addr(cstr_c(m_ip.GetString())); 		if(connect(soc, (sockaddr*)&serveaddress, sizeof(serveaddress))!=0 )
		{
			WSACleanup();
			this->MessageBox ( L"连接服务器端失败!" ) ;
			return;
		}    
	}
	Else
	{
		control_soc= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(control_soc== INVALID_SOCKET)
		{
			WSACleanup();
			this->MessageBox ( L"创建控制socket失败!" ) ;
			return;
		}
		sockaddr_in  serveaddress;
		serveaddress.sin_family = AF_INET;
		serveaddress.sin_port = htons(port);
		serveaddress.sin_addr.S_un.S_addr = inet_addr(cstr_c(m_ip.GetString()));
		if(connect(control_soc, (sockaddr*)&serveaddress, sizeof(serveaddress))!=0 )
		{
			WSACleanup();
			this->MessageBox ( L"作为控制socket连接服务器端失败!" ) ;
			return;
		}    
	}
}
//停止h264解码并且释放所有资源
void CMFCApplication1Dlg::StopH264Decoder()
{
	if(pCodec!=NULL)
	{
	pCodec->close(pCodecCtx);
	}
	if(pFrame)
	{
		av_free(pFrame);  
		pFrame = NULL;  
	}
	if(pRGBFrame)
	{
		av_free (pRGBFrame);
		pRGBFrame=NULL;
	}
	sws_freeContext (pSwsCtx);
	avformat_close_input(&pFormatCtx);  
}
//将解码后的RGB数据加上bitmap的头信息等,并将其显示在picture control上
void CMFCApplication1Dlg::SaveAsBMPandShow(int isSave,CString filePath)
{
	BITMAPFILEHEADER bmpheader;  
    BITMAPINFOHEADER bmpinfo;  
    BITMAPINFO *pBmpInfo;   
    bmpheader.bfType = 0x4d42;  
    bmpheader.bfReserved1 = 0;  
    bmpheader.bfReserved2 = 0;  
    bmpheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);  
    bmpheader.bfSize = bmpheader.bfOffBits +pCodecCtx->width*pCodecCtx->height*24/8;  
    bmpinfo.biSize = sizeof(BITMAPINFOHEADER);  
    bmpinfo.biWidth = pCodecCtx->width;  
    bmpinfo.biHeight =pCodecCtx->height;  
    bmpinfo.biPlanes = 1;  
    bmpinfo.biBitCount = 24;  
    bmpinfo.biCompression = BI_RGB;  
    bmpinfo.biSizeImage = (pCodecCtx->width*24+31)/32*4*pCodecCtx->height;  
    bmpinfo.biXPelsPerMeter = 100;  
	bmpinfo.biYPelsPerMeter = 100;  
	bmpinfo.biClrUsed = 0;  
	bmpinfo.biClrImportant = 0;  
	pBmpInfo = (BITMAPINFO *)new char[sizeof(BITMAPINFOHEADER)];
	memcpy(pBmpInfo,&bmpinfo,sizeof(BITMAPINFOHEADER));
	if(isSave==0)//显示
	{
	CWnd *pWnd=GetDlgItem(IDC_STATIC_PIC); //获得pictrue控件窗口的句柄
	CRect rect;
	pWnd->GetClientRect(&rect); //获得pictrue控件所在的矩形区域
	CDC *pDC=pWnd->GetDC(); //获得pictrue控件的DC
	pDC->SetStretchBltMode(COLORONCOLOR);
	StretchDIBits(pDC->GetSafeHdc(),0,0,rect.Width(),rect.Height(),0,0,pCodecCtx->width,pCodecCtx->height,pRGBFrame->data[0],pBmpInfo,DIB_RGB_COLORS,SRCCOPY);
	Sleep(1);
	}
	if(isSave==1)//c存储
	{
    FILE *r_fp1;
    fopen_s(&r_fp1,cstr_c(filePath),"wb");
	if(r_fp1==NULL)
    printf("open file failed!\n");
	fwrite(&bmpheader, sizeof(bmpheader), 1, r_fp1);  
	fwrite(&bmpinfo, sizeof(bmpinfo), 1, r_fp1);  
	fwrite(pRGBFrameSave->data[0],(pCodecCtx->width)*(pCodecCtx->height)*24/8, 1,r_fp1);  
	fclose(r_fp1);
	}
}
//初始化,控制连接和数据连接
void CMFCApplication1Dlg::OnBnClickedButton5()
{
	ConnectToServer(0);
	ConnectToServer(3334);
	if(soc!=NULL&&control_soc!=NULL)
		this->MessageBox ( L"初始化成功!" );
	else
		this->MessageBox ( L"初始化失败!" );
}
//按下开始,开始监控
void CMFCApplication1Dlg::OnBnClickedButton1()
{
	if (isMonitor )
	{
		char con[10]={"begin"};
	    int i;
	    i=send(control_soc, con, strlen(con), 0);
		CString videoinfo;
		CTime tm;
		tm=CTime::GetCurrentTime();
		videoinfo.Format(_T("接收到的视频宽度为:%d ,接收到的视频高度为:%d     接收到的视频编码类型为:H.264"),pCodecCtx->width,pCodecCtx->height);
		videoinfo=tm.Format(_T("视频监控开始的时间为:%X   "))+videoinfo; //%X是显示时间的时分秒
		m_videoInfo.SetWindowTextW(videoinfo);
	}
	else
	{
		 haveConnected=TRUE;
	    char con[10]={"begin"};
	    int i;
	    i=send(control_soc, con, strlen(con), 0);

	    m_videoInfo.SetWindowTextW(_T("请等待获取视频相关信息....................."));
		HANDLE hThread = CreateThread ( NULL, 0, MonitorThread, this, 0, NULL ) ;
		if ( hThread == NULL )
		{
			this->MessageBox ( L"创建视频监视器失败!" );
		}
		else
		{
			isMonitor=TRUE;
		}
	}
}
//停止显示,关闭连接并释放相关资源
void CMFCApplication1Dlg::OnBnClickedButton2()
{  
		//发送结束命令
		char con[10]={"end"};
	    int i;
	    i=send(control_soc, con, strlen(con), 0);
		m_videoInfo.SetWindowTextW(_T("视频监控已停止,服务器已停止视频采集。"));
}
void CMFCApplication1Dlg::OnBnClickedButton3()
{  
	isShowContinue=FALSE;
	pRGBFrameSave=av_frame_alloc();
	*pRGBFrameSave=*pRGBFrame;
	CString defaultDir = L"E:\\FileTest";	//默认打开的文件路径
	CString fileName = L"1.bmp";			//默认打开的文件名
	CString filter = L"文件*.bmp;";	//文件过虑的类型
	CFileDialog openFileDlg(0, defaultDir, fileName, OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, filter, NULL);
	openFileDlg.GetOFN().lpstrInitialDir = L"C:\\Users\\Administrator\\Desktop\\1.bmp";
	INT_PTR result = openFileDlg.DoModal();
	CString filePath = defaultDir + "\\" + fileName;
	if(result == IDOK) 
	{
		filePath = openFileDlg.GetPathName();
	}
	SaveAsBMPandShow(1,filePath);
	if(pRGBFrameSave)
	{
		av_free(pRGBFrameSave);  
		pRGBFrameSave = NULL;  
	}
}
//继续显示
void CMFCApplication1Dlg::OnBnClickedButton4()
{
	isShowContinue=TRUE;
}

ffmpeg解码H.264视频数据,MFC播放视频_第1张图片


你可能感兴趣的:(c++,编解码,mfc,c++,ffmpeg,开源项目,h.264)