昨天说好的,今天补上PC端程序制作方法。
摄像头照射mini6410 小板上效果图:
首先明确一下PC端程序需要做哪些任务。
1.向ARM端小车发出控制命令
2.接受小车实时传输的图像数据
3.解压H264视频数据
4.快速显示YUV420数据
第 3和4部分要复杂一些。
上位使用的是 Visual C++/MFC
显示视频部分,为了提高视频质量和速度,我使用了DirectX.7
(1)创建MFC
打开VS2005软件,创建一个MFC对话框应用程序(Dialog-based Application),在名称栏输入创建项目的名称,点击“确定”。如下图所示:
在出现的“MFC应用程序向导”对话框内,选择“基于对话框”,并取消“使用Unicode库(N)”其他选项不做修改,单击“下一步”,如下图所示:
单击“下一步”,选择使用windows套接字单击“完成”如下图所示:
即可创建一个MFC对话框。
在工具箱中点击 ,添加此控制键。
同时修改属性中 ID为:IDC_M_PICTURE。
同样方式添加五个按钮如图,按钮的ID让它为默认值吧不去修改了(Button1的ID为IDC_BUTTON1)。
修改按钮的名字,从1到5分别为,“TCP开启服务”,“左转”,“右转”,“前进”,“后退”。
为按键1 (TCP开启服务)按键添加点击事件函数,简单方法双击即可。
为picture控件添加变量,变量名为“m_picture”,操作如图:
点击“完成”。在TCP_H264控制服务端Dlg.h中自己添加:
public:
CStatic m_picture;
添加DircetDraw函数库:
在菜单中选择项目—>属性—>配置属性—>链接器—>输入—>附加依赖项添加“ddraw.lib dinput.lib dxguid.lib” 。如图所示:
提示:(添加附加依赖之前,要在VS2005 菜单中 工具—>选项—>项目和解决方案—>C++目录—>包含文件和库文件分别添加DirectX的 include 和lib所依赖的路径。
在“TCP_H264控制服务端Dlg.h”中添加:#include <ddraw.h>
添加H264 软解码库函数:
该库函数是网上的大神提取 著名的ffmpeg项目的源码。该部分代码功能为将H264压缩数据流,解码成YUV420的图片数据流。
右击“头文件”—>“添加”—>“现有”选中8个头文件(扩展名为.h)。
右击“源文件”—>“添加”—>“现有”选中7个源文件(扩展名为.c),如图:
在“TCP_H264控制服务端Dlg.cpp”中添加:
extern "C"
{
#include "h264lib\avcodec.h"
}
同时取消预编译头文件。
到此对话框的界面设计与编程环境的构建终于完成啦!尤其是编程环境的构建,真烦,记得第一次学习DircetDraw时,为了搭建环境我卡主我好久。其实步骤很简单,但是没人指点,没人引路,就让它变的很难了。
接下来让我们开始进入程序的核心,编写代码!
(1)在“TCP_H264控制服务端Dlg.h”中为“CTCP_H264控制服务端Dlg”类添加 如下代码,
该部分代码添加为CTCP_H264控制服务端Dlg类中的public变量,这些变量为相应的句柄,指针,还有调用DircetDraw TCP H264软解码 等所要用到的结构。
//***************************************************
// 离屏表面
//***************************************************
HWND Phwnd; //picture窗口句柄
HRESULT ddrval; //函数返回值接收变量,以判断韩函数的调用状况
LPDIRECTDRAW lpDD; //DirectDraw对象
LPDIRECTDRAWSURFACE lpDDSPrimary; //DirectDraw主页面
LPDIRECTDRAWSURFACE lpDDSOffScr; //DirectDraw缓冲区,下一帧画面的存储
LPDIRECTDRAWCLIPPER lpClipper; //窗口句柄裁剪表
DDSURFACEDESC ddsd; //页面描述
LPBYTE lpSurface; //数据指针
void InitDricetDraw();
void DrawH264Msg( unsigned char* H264Msg, int fileSize);///绘制H264 函数
//***************************************************
// H264
//***************************************************
int got_picture, consumed_bytes;
struct AVCodecContext *c; // Codec Context
struct AVFrame *picture; // Frame
//***************************************************
// TCP
//***************************************************
SOCKET sockServer; ///服务端句柄
SOCKET sockConn; ///客户端句柄
unsigned char h264msg[50000];
///**************线程****************
HANDLE hThread1;///线程句柄
bool hThread1_ToEnd;///标示
///**************************
//鼠标按下
///**************************
CButton *button1;
CButton *button2;
CButton *button3;
CButton *button4;
( 2 )在在“TCP_H264控制服务端Dlg.h”中为
void InitDricetDraw();
void DrawH264Msg( unsigned char* H264Msg, int fileSize);
写出实体。
void InitDricetDraw()函数的实体:
函数的功能为:离屏表面初始化,通过句柄lpDD(DirectDraw对象)对其进行创建和设置,并且获得lpDDSPrimary 的DirectDraw主页面。
lpDD对象,表示显示硬件,它包括了显示器和显卡还有显存,用它来代表整个显示系统。
lpDDSPrimary 对象代表了一个页面。页面可以有很多种表现形式,它既可以是可见的(屏幕的一部分或全部),称之为主页面(Primary Surface);也可以是作换页用的不可见页面,称之显卡后台缓存(Back Buffer),在换页后,它成为可见;还有一种始终不可见的,称之为离屏页面(Off-screen Surface),用它来存储图象。其中,最重要的页面是主页面,每个DirectDraw应用程序都必须创建至少一个主页面,用它来代表屏幕上可见的区域,说白了,就是你的显示屏幕。
/*************************************************************/
// 窗口模式初始化DirectDraw的函数
/*************************************************************/
void CTCP_H264控制服务端Dlg::InitDricetDraw()
{
//创建DirectDraw主对象
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if( ddrval != DD_OK ){
MessageBox("DirectDrawCreate不支持");
}
//用DDSCL_NORMAL表示我们要与GDI共存
ddrval = lpDD->SetCooperativeLevel( Phwnd, DDSCL_NORMAL );
if( ddrval != DD_OK ){
lpDD->Release();
MessageBox("SetCooperativeLevel不支持");
}
//描述主表面,对其进行设置
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
// 不是可翻
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval != DD_OK ){
lpDD->Release();
MessageBox("CreateSurface不支持");
}
// 创建一个裁切板保证不画到窗口外面
ddrval = lpDD->CreateClipper( 0, &lpClipper, NULL );
if( ddrval != DD_OK ){
lpDDSPrimary->Release();
lpDD->Release();
MessageBox("CreateClipper不支持");
}
// 把窗口句柄设置给裁切板就给了它一个与窗口相适的裁切框
ddrval = lpClipper->SetHWnd( 0, Phwnd );
if( ddrval != DD_OK ){
lpClipper->Release();
lpDDSPrimary->Release();
lpDD->Release();
MessageBox("// 把窗口句柄给裁切板不支持");
}
//把裁切板附到主页面
ddrval = lpDDSPrimary->SetClipper( lpClipper );
if( ddrval != DD_OK ){
lpClipper-> Release();
lpDDSPrimary->Release();
lpDD->Release();
MessageBox("lpDDSPrimary->SetClipper( lpClipper );不支持");
}
//描述缓冲表面,对其进行设置
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH|DDSD_PIXELFORMAT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 176;///图片宽度
ddsd.dwHeight = 144;///图片高度
ddsd.ddpfPixelFormat.dwSize=sizeof(DDPIXELFORMAT);
ddsd.ddpfPixelFormat.dwFlags=DDPF_FOURCC|DDPF_YUV;
ddsd.ddpfPixelFormat.dwFourCC=MAKEFOURCC('I','Y','U','V');//图片像素存储格式YUV420('I','Y','U','V')
ddsd.ddpfPixelFormat.dwYUVBitCount=8;
// 单独创建后缓冲页
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSOffScr, NULL );
if( ddrval != DD_OK ){
lpClipper-> Release();
lpDDSPrimary->Release();
lpDD->Release();
MessageBox("单独创建后缓冲页不支持");
}
//H264软解码初始化
c = avcodec_alloc_context();
if(!c)
MessageBox("// avcodec_alloc_context不支持");
if (avcodec_open(c) < 0)
MessageBox("// avcodec_open不支持");
picture = avcodec_alloc_frame();
if(!picture)
MessageBox("// avcodec_alloc_frame不支持");
}
void DrawH264Msg( unsigned char* H254Msg, int fileSize);函数实体:
函数功能:通过decode_frame()函数对H264数据进行软解码,相应的图片YUV420数据指针存在picture变量中,然后 向显卡缓冲区(lpDDSOffScr)中填充YUV420数据,然后通过lpDDSPrimary->Blt(&rctDest,lpDDSOffScr,&rctSour,DDBLT_WAIT,NULL)把lpDDSOffScr缓冲区中是数据(下一帧的画面)翻转到主表面lpDDSPrimary上,即在屏幕上显示画面。
/**************************************************/
// 离屏表面绘制代码
/**************************************************/
void CTCP_H264控制服务端Dlg::DrawH264Msg( unsigned char* H264Msg, int fileSize)
{
//调用软解码库API函数decode_frame()对存储在H264Msg的h264数据进行解码,解码出的YUV420数据存在picture中
consumed_bytes= decode_frame(c, picture, &got_picture, H264Msg, fileSize);
//锁定缓冲表面内存数据,表示要对其进行修改填充,并获得缓冲内存指针存在ddsd结构中
ddrval=lpDDSOffScr->Lock(NULL,&ddsd,DDLOCK_WAIT|DDLOCK_WRITEONLY,NULL);
while(DDERR_WASSTILLDRAWING==ddrval);
if(DD_OK!=ddrval){
MessageBox("离屏表面锁定不成功!");
}
//指向缓冲表面的指针
LPBYTE lpSurf=(LPBYTE)ddsd.lpSurface;
//通过指针想缓冲中填充YUV420图片是数据
int i=0;
for(i=0;i<ddsd.dwHeight;i++){
memcpy(lpSurf,picture->data[0],ddsd.dwWidth);
picture->data[0]+=208;
lpSurf+=ddsd.lPitch;
}
for(i=0;i<ddsd.dwHeight/2;i++){
memcpy(lpSurf,picture->data[1],ddsd.dwWidth/2);
picture->data[1]+=104;
lpSurf+=ddsd.lPitch/2;
}
for(i=0;i<ddsd.dwHeight/2;i++){
memcpy(lpSurf,picture->data[2],ddsd.dwWidth/2);
picture->data[2]+=104;
lpSurf+=ddsd.lPitch/2;
}
//释放锁定,修改完成,允许被其他函数调用
lpDDSOffScr->Unlock(NULL);
RECT rctDest; //目标区域
RECT rctSour; //源区域
// 首先需要指出主页面的位置
rctSour.left=0;
rctSour.top=0;
rctSour.right=ddsd.dwWidth;
rctSour.bottom=ddsd.dwHeight;
::GetClientRect(Phwnd,&rctDest);
::ClientToScreen(Phwnd,(LPPOINT)&rctDest.left);
::ClientToScreen(Phwnd,(LPPOINT)&rctDest.right);
// 窗口模式blit,将缓冲表面的图片数据赋主表面,即YUV420图片数据就显示在屏幕上了
ddrval=lpDDSPrimary->Blt(&rctDest,lpDDSOffScr,&rctSour,DDBLT_WAIT,NULL);
while(DDERR_WASSTILLDRAWING==ddrval);
if(DD_OK!=ddrval){
MessageBox("Blt到主表面不成功");
}
}
( 3 )在TCP_H264控制服务端Dlg.cpp中 添加全局函数:
DWORD WINAPI Fun1Proc(LPVOID lpParameter); 线程函数。
函数作用:该函数是该程序工作时的逻辑主体,进入后TCP进行监听,链接,接收数据,数据处理绘制,发送数据等。
实体如下:
/****************************************************************************/
// 线程函数,当TCP开启时,程序运行在本函数中,也是该程序的应用程序主体
/****************************************************************************/
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
CTCP_H264控制服务端Dlg *pdlg= (CTCP_H264控制服务端Dlg*)lpParameter;
//创建套接字
pdlg->sockServer = socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM参数设置为TCP连接
SOCKADDR_IN addrServer; //设置服务器端套接字的相关属性
SOCKADDR_IN addrClient; //用来接收客户端的设置,包括IP和端口
addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY); //设置IP
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(8080); //设置端口号
//将套接字绑定到本地地址和指定端口上
bind( pdlg->sockServer, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));
//将套接字设置为监听模式,并将最大请求连接数设置成,超过此数的请求全部作废
listen( pdlg->sockServer, 5);
int len=sizeof(SOCKADDR);
int filesie=0;
int dd,cc;
while(1) //不断监听
{
//得到创建连接后的一个新的套接字,用来和客户端进行沟通,原套接字继续监听客户的连接请求
pdlg->sockConn = accept( pdlg->sockServer, (SOCKADDR*)&addrClient, &len);
if( (pdlg->sockConn) != (INVALID_SOCKET)) //创建成功
{
if( pdlg->hThread1_ToEnd){
return 1;///结束线程
}
while(1){
//获取H264数据的长度信息
recv(pdlg->sockConn,(char *)&filesie,4,0);
if(filesie<100){
pdlg->MessageBox("数据大小有问题!");
}
dd=0;
cc=0;
//接收长度为filesie的H264数据
while( dd< filesie){
cc = recv( pdlg->sockConn, (char *)pdlg->h264msg, filesie- dd,0);
dd = dd + cc;
}
//绘制H264视频
pdlg->DrawH264Msg(pdlg->h264msg,filesie);
}
}
else{
pdlg->MessageBox("创建连接失败");
return 0;
}
}
return 0;
}
(4)窗口初始化函数BOOL CTCP_H264控制服务端Dlg::OnInitDialog()中添加如下代码:
// TODO: 在此添加额外的初始化代码
/*****************************************************/
// 离屏表面初始化
/*****************************************************/
Phwnd = m_picture.GetSafeHwnd();
InitDricetDraw();
(5)为按钮1(TCP开启)处理函数添加代码
void CTCP_H264控制服务端Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
hThread1_ToEnd = 0;
hThread1 = CreateThread(NULL,0,Fun1Proc,(LPVOID)this,0,NULL);
CloseHandle(hThread1);
}
(6)添加void CTCP服务端解析H264Dlg::OnDestroy()函数,当窗口关闭时处理:
void CTCP_H264控制服务端Dlg::OnDestroy()
{
CDialog::OnDestroy();
// TODO: 在此处添加消息处理程序代码
hThread1_ToEnd = TRUE;//通知线程结束标示
//释放DirectDraw对象
lpClipper->Release();
lpDDSPrimary->Release();
lpDD->Release();
closesocket(sockConn); //将本次建立连接中得到套接字关闭
closesocket(sockServer); //对一直处于监听状态的套接字进行关闭
delete button1;
delete button2;
delete button3;
delete button4;
}
(7)对了,刚想起了,我们在对话框装设置时,摆放了5个按钮,而我只为其中的第一个(TCP开启)按钮添加了点击事件处理函数,而后四个按钮没有添加任何事件处理函数,现在我们回头说一下:
后四个按钮是控制前后左右的,我本来的打算是为后四个按钮添加按钮“按下”,“抬起”消息。在按下按钮时,想ARM板发送“前进”信息,按钮抬起发送“前进结束”消息。可是MFC按钮控件没有提供这样的这样的事件处理函数,没办法只要自己写了。
右击 “工程” 选择添加—>类—>MFC类,单击添加。
输入类名CMyButton(当然个可以随便设),基类选择CButton(必须是这个)。如图:
单击完成。
然后在MyButton.h与MyButton.cpp中重写CMyButton类为:
(1)在MyButton.h中声明CTCP_H264控制服务端Dlg,重写构造函数,同时添加鼠标左键按下消息,和鼠标左键抬起消息,添加代码如下:
class CTCP_H264控制服务端Dlg;
class CMyButton : public CButton
{
DECLARE_DYNAMIC(CMyButton)
public:
CMyButton(CTCP_H264控制服务端Dlg *pd);
virtual ~CMyButton();
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
public:
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
};
(2)在MyButton.cpp中为重写的构造函数写实体,为鼠标左键按下消息(OnLButtonDown)鼠标左键抬起消息(OnLButtonUp)写实体,代码如下:
构造函数实体:
函数功能:在创建CMyButton类时,需要按参数形式,把主窗体句柄传递过来,从而就可以调用到主窗体的资源了。
CMyButton::CMyButton(CTCP_H264控制服务端Dlg *pd)
{
pdlg=pd;
}
OnLButtonDown函数的实体:
函数功能:获取按钮控件本身ID,判断是哪个按钮按下,并且TCP函数send()相应的数据信息。
void CMyButton::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CButton::OnLButtonDown(nFlags, point);
myID = GetDlgCtrlID();
switch( myID )
{
case IDC_BUTTON2:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=1;
ser_order[3]=0;
ser_order[4]=1;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON3:
ser_order[0]=1;
ser_order[1]=1;
ser_order[2]=0;
ser_order[3]=1;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON4:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=1;
ser_order[3]=1;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON5:
ser_order[0]=1;
ser_order[1]=1;
ser_order[2]=0;
ser_order[3]=0;
ser_order[4]=1;
send( pdlg->sockConn , ser_order , 6,0);
break;
default :
break;
}
}
OnLButtonUp函数的实体:
函数功能:获取按钮控件本身ID,判断是哪个按钮按下,并且TCP函数send()相应的数据信息。
void CMyButton::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CButton::OnLButtonUp(nFlags, point);
myID = GetDlgCtrlID();
switch( myID )
{
case IDC_BUTTON2:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=0;
ser_order[3]=0;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON3:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=0;
ser_order[3]=0;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON4:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=0;
ser_order[3]=0;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
case IDC_BUTTON5:
ser_order[0]=1;
ser_order[1]=0;
ser_order[2]=0;
ser_order[3]=0;
ser_order[4]=0;
send( pdlg->sockConn , ser_order , 6,0);
break;
default :
break;
}
}
(3)最后在TCP_H264控制服务端Dlg.cpp中添加MyButton.h头文件,同时在主窗体构造函数添加如下代码:
button1 = new CMyButton(this);
button2 = new CMyButton(this);
button3 = new CMyButton(this);
button4 = new CMyButton(this);
button1->SubclassDlgItem(IDC_BUTTON2,this);///前进
button2->SubclassDlgItem(IDC_BUTTON3,this);///后退
button3->SubclassDlgItem(IDC_BUTTON4,this);///左转
button4->SubclassDlgItem(IDC_BUTTON5,this);///右转
到此我们的程序已经全部写完啦!
让我们检验一下刚刚写好的程序!
环境:在ARM11开发板(由于车模还没有完成先在实验板上看看视频的效果)上运行已经也好的TCP_视频采集程序,通过网线向我们的电脑上发送视频数据,我们在PC电脑上用我们刚刚写好的程序接收视频数据并通过DirectX显示,效果如图: