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
(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; 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;
}