本文旨在剖析开发基于P2P技术开发视频会议软件相关主要技术,并给出一个简单的例子。
一、 引言
我相信多数人听说过微软的NetMeeting,甚至有人直接使用过;而如今,众多的网虫沉迷于视频聊天。这类软件是怎样开发出来的呢?本文中,让我们来共同剖析开发基于P2P技术开发视频会议软件相关的主要技术,并给出一个简明的例子。本示例应用程序允许LAN/Intranet上的任何两个人举行视频会议。
凭直觉我们就会知道,开发这一类软件所涉及的主要问题,就是视频帧的大尺寸将极大地影响数据的传输质量。因而,这类软件的性能也主要依赖于视频帧编码和解码的质量。为此,在本例中,我们选用的是较快速的H.263编码器库,该库具有相当好的压缩比率,从而有效地克服了我们在图像传输中的速度矛盾。
请注意,有兴趣的读者可稍微修改本文中的示例程序以应用于因特网环境中。
二、 音频的录制与播放问题
这一部分的开发相对简单。其一,这种功能的API从Windows 3.1开始就已经提供(winmm.lib+mmsystem.h);其二,如今借助于方便的因特网,我们完全可以搜到现成的包装类。在本文中,我们直接借用了提供了两个现成的RecordSound与PlaySound类。这两个类都派生于CWinThread类,用户可以“死搬硬套”地使用它们。下面代码展示了这两个类的使用,具体包装类定义请参考下载源码文件。
//创建并启动录音线程
record=new RecordSound(this);
record->CreateThread();
//创建并启动播放线程
play=new PlaySound1(this);
play->CreateThread();
//开始录制
record->PostThreadMessage(WM_RECORDSOUND_STARTRECORDING,0,0);
//开始播放
play->PostThreadMessage(WM_PLAYSOUND_STARTPLAYING,0,0);
//在音频录制期间,我们可以在RecordSound类的OnSoundData
//回调函数中使用这些数据。在此,你可以放置你要发送到远程宿主的数据……
//播放接收自远程宿主的音频数据
play->PostThreadMessage(WM_PLAYSOUND_PLAYBLOCK,size,(LPARAM) data);
//停止录制
record->PostThreadMessage(WM_RECORDSOUND_STOPRECORDING,0,0);
//停止播放
play->PostThreadMessage(WM_PLAYSOUND_STOPPLAYING,0,0);
//最后,停止录音线程
record->PostThreadMessage(WM_RECORDSOUND_ENDTHREAD,0,0);
//停止播放线程
play->PostThreadMessage(WM_PLAYSOUND_ENDTHREAD,0,0);
上面已经加了注释,使用方法一目了然。
三、 视频捕获的问题
当前,在Windows平台下开发视频应用一般采用两种方案。一种是基于视频采集卡所附带的二次软件开发包SDK进行。此方式的优点:帮助资料齐全,直接套用现成的API,易于上手;但缺点也是明显的:硬件依赖性强,缺乏应有的灵活性,因此,不能充分满足开发通用的视频应用的需要。
另一种方案是基于微软公司的VFW(Video for Windows)进行。这个SDK为开发Windows平台下的视频应用程序提供也现成的软件工具包(一组API),开发人员可以通过它们很方便地实现视频捕获、视频编辑及视频播放功能,特别是可利用其中内置的回调函数开发出更为复杂的视频应用程序。因此,这种方案的优点是播放视频时不需要专用的硬件设备(大多数的视频采集卡驱动程序都支持VFW接口),应用灵活,可以满足视频应用程序开发的需要。值得庆幸的是,如今的Windows版本都内置安装了VFW相关组件,而VC++自4.0以来就支持VFW,从而大大简化了视频应用程序的开发。目前,基于PC的多媒体应用程序的视频部分,大都是利用VFW API开发的。
VFW以消息驱动方式实现对视频设备进行访问,便于开发者控制设备数据流的工作过程。简言之,这个框架主要包括VICAP.DLL、MSVIDEO.DLL、MCIAVI.DRV、AVIFILE.DLL、ICM、ACM等多个动态连接库,这些组件协同合作,共同完成视频的捕获、视频压缩及播放功能。有关这些模块的具体介绍见MSDN,在此略过。
(一)视频捕获
视频数据的实时采集,主要通过AVICAP模块中的消息、宏函数、结构以及回调函数来完成。视频捕获的大致过程如下:
(1)建立捕获窗口
利用函数capCreateCaptureWindow()建立视频捕获窗口,它是所有捕获工作及设置的基础。其主要功能包括:①动态地同视频和音频输入器连接或断开;②设置视频捕获速率;③提供视频源、视频格式以及是否采用视频压缩的对话框;④设置视频采集的显示模式为Overlay或为Preview;⑤实时获取每一帧视频数据;⑥将一视频流和音频流捕获并保存到一个AVI文件中;⑦捕获某一帧数字视频数据,并将单帧图像以DIB格式保存;⑧指定捕获数据的文件名,并能将捕获的内容拷贝到另一文件。
(2)登记回调函数
登记回调函数用来实现用户的一些特殊需要。在以一些实时监控系统或视频会议系统中,需要将数据流在写入磁盘以前就必须加以处理,达到实时功效。应用程序可用捕获窗来登记回调函数,以便及时处理以下情况:捕获窗状态改变、出错、使用视频或音频缓存、放弃控制权等,相应的回调函数分别为capStatusCallback(),capErrorCallback(),capVideoStreamCallback(),capWaveStreamCallback(),capYieldCallback()。
(3)获取捕获窗口的缺省设置
通过宏capCaptureGetSetup(hWndCap,&m_Parms,sizeof(m_Parms))来完成。
(4)设置捕获窗口的相关参数
通过宏capCaptureSetSetup(hWndCap,&m_Parms,sizeof(m_Parms))来完成。
(5)连接捕获窗口与视频捕获卡
通过宏capDriveConnect(hWndCap,0)来完成。
(6)获取采集设备的功能和状态
通过宏capDriverGetCaps(hWndCap,&m_CapDrvCap,sizeof(CAPDRIVERCAPS))来获取视频设备的能力,通过宏capGetStatus(hWndCap,&m_CapStatus,sizeof(m_CapStatus))来获取视频设备的状态。
(7)设置捕获窗口显示模式
视频显示有Overlay(叠加)和Preview(预览)两种模式。在叠加模式下,捕获视频数据布展系统资源,显示速度快,视频采集格式为YUV格式,可通过capOverlay(hWndCap,TRUE)来设置;预览模式下要占用系统资源,视频由系统调用GDI函数在捕获窗显示,显示速度慢,它支持RGB视频格式。
(8)捕获图像到缓存或文件并作相应处理
若要对采集数据进行实时处理,则应利用回调机制,由capSetCallbackOnFrame(hWndCap,FrameCallbackProc)完成单帧视频采集;由capSetCallbackOnVideoStream(hWndCap,VideoCallbackProc)完成视频流采集。如果要保存采集数据,则可调用capCaptureSequence(hWnd);要指定文件名,可调用capFileSetCapture(hwnd,Filename)。
(9)终止视频捕获断开与视频采集设备的连接
调用capCatureStop(hWndCap)停止采集,调用capDriverDisconnect(hWndCap),断开视频窗口与捕获驱动程序的连接。
由于上面这些API密切相关,所以为了使用方便,我们干脆把它们打包到一个视频捕获类VideoCapture中。
下面的代码片断展示了这个类的使用思路:
//创建视频捕获类的实例
vidcap=new VideoCapture();
//当帧捕获完成时,下面这一句将用于调用主对话框类的显示函数
vidcap->SetDialog(this);
//下一行完成初始化工作:连接到驱动程序;设置使用的视频格式等。
//如果成功地连接到视频捕获设备返回TRUE。
vidcap-> Initialize();
//如果连接成功,那么,我们就可以得到与视频格式相关的BITMAPINFO
//结构。后面将用之显示捕获的帧
this->m_bmpinfo=&vidcap->m_bmpinfo;
//现在,你可以正式开始视频捕获了……
vidcap->StartCapture();
//一旦捕获开始,捕获的帧将到达回调函数—VideoCapture类的OnCaptureVideo函数。
//在此回调函数中,你可以调用显示函数实现帧显示(见下一节)
//停止捕获
vidcap->StopCapture();
//成功捕获后,释放视频捕获类
vidcap->Destroy();
【注意】为了顺利编译和链接,你需要在类实现文件(VideoCapture.cpp)的前面加上如下语句:
#pragma comment(lib,"vfw32")
#pragma comment(lib,"winmm")
(二)显示捕获的视频帧
对于显示捕获的视频帧方面(也就是显示图像的问题),显然存在多种方案。例如,我们可以使用SetDIBitsToDevice()方法实现直接显示捕获的视频帧。但是,这种方案速度非常慢,因为它是基于图形设备接口(GDI)的函数。相比之下,更好一些的方法是使用DrawDib API来绘制帧,因为这个函数可以直接写向视频内存,因此能够提供更好的性能。
下面的代码片断展示了如何使用DrawDib函数显示捕获的视频帧:
//初始化DIB以便绘制
HDRAWDIB hdib=::DrawDibOpen();
//然后,使用适当的参数调用这个函数……
::DrawDibBegin(hdib,...);
//现在,已经作好准备—可以调用这个函数进行帧显示了
::DrawDibDraw(hdib,...);
//最后,结束帧绘制
::DrawDibEnd(hdib);
::DrawDibClose(hdib);
其实,上面代码非常类似普通位图绘制过程。
四、 选择适当的编码/解码库
在本文中,我们选用Roalt Aalmoes的开源的快速H.263编码器库。
(一) 使用编码器代码示例
//初始化压缩器
CParam cparams;
cparams.format = CPARAM_QCIF;
InitH263Encoder(&cparams);
//如果你需要从RGB24转换到YUV420格式,那么应该调用下面的函数
InitLookupTable();
//创建回调函数
//OwnWriteFunction是编码期间返回编码数据时调用的全局函数
WriteByteFunction = OwnWriteFunction;
//压缩数据必须使用YUV420格式
//在压缩之前调用下面这个方法
ConvertRGB2YUV(IMAGE_WIDTH,IMAGE_HEIGHT,data,yuv);
//压缩帧……
cparams.format=CPARAM_QCIF;
cparams.inter = CPARAM_INTRA;
cparams.Q_intra = 8;
cparams.data=yuv; //数据是YUV格式
CompressFrame(&cparams, &bits);
//你可以从开始时你已经注册的回调函数中取得压缩的数据
//最后,终止编码器
// ExitH263Encoder();
(二) 解码器编程
注意,原始的H.263编码器库以C方式进行编码,而且提供了其它更多的细节实现。在本文中,我们以C++重新进行了改写。
下面是解码器的使用示例代码框架:
//初始化解码器
InitH263Decoder();
//解压帧……
//rgbdata必须足够大以便存储输出数据;
//解码器以YUV420格式生成图像数据;
//解码之后,把它再转换成RGB24格式……
DecompressFrame(data,size,rgbdata,buffersize);
//最后一步,终止解码器
ExitH263Decoder();
五、 运行应用程序
为了试验本文示例应用程序,你应该把可执行文件复制到一个LAN中的两台不同的机器上;然后,分别运行之。从一台机器上选择“连接”菜单项,并在弹出对话框内输入另一台机器的名字或IP地址,最后点击“连接”按钮。此时,在另一台机器上应该弹出一个“接受/拒绝”的对话框窗口,点击“接受”按钮。之后,在第一台机器上将显示通知对话框。按“OK”即可开始你的视频会议(聊天……)了。
六、 小结
我想通过本文向读者强调如下问题:Windows平台下的音频及视频开发并非那么复杂高深;有了本文的基础,你也完全可以据需要开发出自己的视频会议、实时监控系统、视频聊天等软件;另外,本文介绍的技术也可经修改并应用于因特网平台上。
同时,我们还看到微软的数字视频处理软件开发包Video for Windows的确为我们帮了大忙,而借助于因特网上的开源多媒体包能更快地加快这类软件的开发。