Barefoot
视频聊天系统作为一种新型的通信和交流工具,突破了地域的限制,可以提供更为便捷、灵活、全面的音、视频信息的传递和服务,具有极其广泛的发展前景。
本文介绍了采用JAVA编程开发视频聊天系统的一套比较常用的解决方案。文字聊天采用TCP模式;语音视频聊天采用UDP模式,在客户端之间点对点的进行。在该方案中,通过函数库VFW来实现视频捕获、影像压缩以及影像播放。微软公司提供的专门用于视频捕获开发的工具包VFW,为在Windows操作系统中实现视频捕获提供了标准的接口,从而大大降低了程序的开发难度。在视频传输方面,则通过组建视频帧,将位图形式的视频帧压缩成帧格式的Mpeg4流,传输到客户端后,解压并显示影像。同时,在本方案中,采用了线程来实现语音录制和语音回放,最终实现了通过服务器中转的文字聊天、点对点的语音视频聊天。
关键词:文字聊天;VFW;视频捕获;视频传输;语音录制;语音回放
在文字聊天时,服务器端与客户端的连接是采用的TCP套接节进行连接。TCP套接字的使用如图5。创建CSocket对象CSocketServer来处理服务器端与客户端的连接,CSocket继承于CasyncSocket,是Windows Socket API的高层抽象。CSocket通常和CsocketFile以及Carchive类混合使用,这两个类负责数据的发送和接收。要使用CSocket对象,首先要调用构造函数,然后调用Create函数创建一个Socket句柄。CSocket函数缺省是创建一个流Socket;如果没有使用CArchive类,那么还可以创建一个数据报Socket。服务器端调用Accept,客户端调用Connect,然后创建一个CsocketFile去关联CSocket。接下来的操作可以创建CArchive对象关联CsocketFile,以用来发送和接收数据。
ChatServer服务器运行时,利用一个CSocket对象CSocketServer启动服务器,用函数gethostname来获得服务器端主机名和IP,同时在服务器对话框中显示服务器IP,并将分配的固定端口号8123显示在对话框中。用一个list列表显示在线用户,随时更新用户登录情况,用一个edit box显示客户端的聊天内容以及系统提示消息。每一个ChatClient客户端启动时,利用服务器内定的用户号和密码来登录(如图6)。在整个系统中,利用链表来处理所有的用户信息:当有用户登录时,在链表尾部加入该用户信息;当用户下线时,在该链表中删除用户,并提示所有用户,该用户下线。在对链表进行操作的同时,要更新list列表中的信息。
用户登录界面:
ChatServer服务器端响应客户端文字聊天时的中转情况如下图:
ChatClient客户端两两间进行文字聊天时,发送信息的处理函数如下:
void CChatClientDlg::OnChatBtSend() //发送信息按键
{
if( !m_bConnect)
{
SetMessageBox("请连接服务器!\r\n");
return ;
}
CString str;
CString szUserName;
CMesg msg;
GetDlgItemText(IDC_MESSAGE,str);
GetDlgItemText(IDC_USERNAME,szUserName);
if( str.GetLength() <= 0 )
{
SetMessageBox("请输入想要发送的信息!\r\n");
return ;
}
if ( szUserName.GetLength() <= 0)
{
SetMessageBox("请选择说话对象!\r\n");
return ;
}
//消息封装
msg.m_szCommand.Format("Message");
msg.m_szRecObject.Format(szUserName);
msg.m_szText.Format(str);
m_csClient->SendM(&msg);
AddReceiver(szUserName , true);
AddChatMessage(str);
}
在聊天两个客户端的信息情况如下图。
在实现语音视频聊天时,采用的是基于UDP套接字的点对点模式,而UDP面向的是无连接的数据服务,其套接字的使用如下图所示。
利用VFW接口,视频捕获可以分为以下几个步骤:
建立视频采集窗口:该窗口用来接收视频捕捉驱动程序传来的数据和消息
连接视频驱动程序:将建立的视频捕捉窗口与视频设备驱动程序相连
视频捕获初始化
视频捕捉设置:VFW下视频捕捉参数的设置可以通过调用函数或弹出对话框的形式来实现。一般视频驱动程序允许设置的参数包括视频源选择、视频格式、视频显示格式等
设置回调函数:通过回调函数来通知程序视频事件的发生,比如捕捉一帧图像成功的消息,捕捉出错的消息等
结束捕捉:结束捕捉是应该有一些清除工作。如释放分配的内存,断开捕捉窗口与视频捕捉驱动程序的连接,清除视频捕捉窗口等
窗口类为捕获数字视频流及其相关操作提供了很大的方便,灵活编写其中的回调函数可满足实时视频传输的需要,例如应用程序可直接从缓冲中取得数字视频并对其进行压缩编码后实时地传到远端的客户端。
在VC++中,采用VFW技术,客户端通过capSetCallbackOnFrame()注册回调函数,当采集卡采集到一幅图像后,系统就会自动调用回调函数,然后再回调函数中使用ICSeqCompressFrame()函数进行压缩。然后再通过Winsock将压缩后的数据发送到另一客户端。该客户端接收完一帧以后,交给ICDecompress()解压,最后用SetDIBitsToDevice()将图像显示出来。
基本的捕获设置包括设置捕获速度(每秒捕获多少帧)、是否同时捕获声频、捕获缓冲、允许最大丢失多少帧和是否使用DOS内存,以及使用键盘的哪个键或鼠标的哪个键来终止捕获等内容,这些设置使用CAPTUREPARAMS结构来描述,capCaptureGetSetup宏来得到当前的设置,然后改变此结构的成员变量,再使用capCaptureSetSetup宏设置新的设置。
设置捕获速度,通过使用capCaptureGetSetup宏来得到当前的捕捉速度,将当前的捕捉速度保存在CAPTUREPARAMS结构的dwRequestMicroSecPerFrame成员变量中,也可以通过设置此变量来改变当前设置值。
设置终止捕获,同样通过使用capCaptureGetSetup宏来得到当前的设置,当前按键设置保存在CAPTUREPARAMS结构的vKeyAbort成员中,鼠标设置保存在fAbortLeftMouse和fAbortRightMouse成员中,通过修改可以设置新的热健或者鼠标左右键,修改完成后,使用capCaptureSetSetup宏来进行更新。
捕获的时间限制,用CAPTUREPARAMS结构中的fLimitEnabled表示捕获是否有时间的限制,wTimeLimit用来设置指示捕获最大的持续时间,其单位为秒。使用capCaptureGetSetup宏来得到当前的设置值。
下面程序为设置CAPTUREPARAMS结构的实现代码:
BOOL VideoCapture::SetCapturePara()
{
CAPTUREPARMS CapParms={0};
capCaptureGetSetup(m_capwnd,&CapParms,sizeof(CapParms));
//得到当前的捕获速度
CapParms.fAbortLeftMouse = FALSE;
CapParms.fAbortRightMouse = FALSE;
CapParms.fYield = TRUE;
CapParms.fCaptureAudio = FALSE;
CapParms.wPercentDropForError = 80;
if(!capCaptureSetSetup(m_capwnd,&CapParms,sizeof(CapParms)))
{
// log.WriteString("\n Failed to set the capture parameters ");
return FALSE;
}
// Set Video Format
capGetVideoFormat(m_capwnd,&m_bmpinfo,sizeof(m_bmpinfo));
m_bmpinfo.bmiHeader.biWidth=IMAGE_WIDTH;
m_bmpinfo.bmiHeader.biHeight=IMAGE_HEIGHT;
BOOL ret=capSetVideoFormat(m_capwnd,&m_bmpinfo,sizeof(m_bmpinfo));
// log.WriteString("\n Video parameters set properly");
return ret;
}
//终止一个捕获任务
BOOL VideoCapture::StopCapture()
{
capCaptureStop(m_capwnd);
capCaptureAbort(m_capwnd);
Sleep(500);
return TRUE;
}
在捕获前必须创建一个捕获窗口(CaptureWidnow),下面介绍有关捕获窗口的情况:创建一个AVICap捕获窗口,用capCreateCaptureWindow函数并返回一个句柄。将捕获窗口连接至捕获设备,用capDriverConnect函数来使一个捕获窗口与一个捕获设备连接或关联连接上后,就可以通过捕获窗口向捕获设备发送各种消息,可以使用函数capGetDriverDescription来获得已安装的捕获设备名称及版本,将其列举在实现程序过程中。再利用capDriverGetName函数来得到捕获设备的名称将获得的版本发送到capDriverGetVersion。如果断开捕获窗口与捕获设备的连接用capDriverDisconnect。
捕获窗口的状态,用capGetStatus函数来获得当前捕获窗口的状态,得到一个CAPSTATUS结构的拷贝。该结构的内容包含了图片的尺寸、卷轴的当前位置、overlay和preview是否已设置。由于其信息是动态的,每当捕获的视频流的尺寸发生改变,程序应该在获取捕获设备的视频格式以后及时进行刷新。而捕获窗口尺寸的改变并不影响实际的捕获视频流的尺寸。该尺寸由视频捕获设备的格式和视频对话框决定。
//捕获窗口
BOOL VideoCapture::Initialize()
{
char devname[128]={0},devversion[128]={0};
int index=0;
BOOL ret = TRUE, ret1 = TRUE, ret2 = TRUE, ret3 = TRUE;
TRACE("VideoCapture::Initialize\n");
//创建一个AVICap捕获窗口
m_capwnd = capCreateCaptureWindow("Capture",WS_POPUP,0,0,1,1,0,0);
if(!m_capwnd)
{
return FALSE;
}
//connect callback functions
ret = capSetUserData(m_capwnd,this);
//Change destroy functions also........
ret1 = capSetCallbackOnVideoStream(m_capwnd,OnCaptureVideo);
//得到已安装的捕获设备的名称及版本
ret2 = capGetDriverDescription(index,devname,100,devversion,100);
// Connect to webcam driver
//使一个捕获窗口与一个捕获设备连接或关联
ret3 = capDriverConnect(m_capwnd,index);
if(!(ret && ret1 && ret2 && ret3))
{
// Device may be open already or it may not have been
// closed properly last time.
AfxMessageBox("Unable to open Video Capture Device");
// log.WriteString("\n Unable to connect driver to the window");
m_capwnd=NULL;
return FALSE;
}
// Set the capture parameters
if(SetCapturePara()==FALSE)
{
// log.WriteString("\n Setting capture parameters failed");
capDriverDisconnect(m_capwnd); //使捕获窗口与一个捕获设备断开
return FALSE;
}
return TRUE;
}
视频捕获必须具有视频捕获驱动才能进行,其相关内容如下:
视频捕获驱动的性能,capDriverGetCap函数得到当前连接视频驱动的硬件性能,该信息保存在CAPDRIVERCAPS结构中;视频对话框,每个视频驱动能够提供4个对话框来控制视频捕获和数字化处理视频对话框定义的视频压缩率和图像品质等。视频对话框都在视频捕获驱动中定义。这个四个对话框分别为:Video Source对话框用于控制选择视频来源(capDlgVideoSource);Video Format对话框定义视频帧的尺寸和精度,以及视频捕获卡的压缩设置(capDlgVideoFormat);Video Display对话框控制在视频捕获期间相关显示器上的显示(capDlgVideoDisplay);Video Compression对话框控制压缩和图像品质(caoDlgVideoCompression)。
在音频的录制和播放时,采用的用户界面线程来处理,是CWinThread对象,根据前面线程的介绍,一步一步的来实现。录音用的一个CWinThread对象CAudioRec来实现,部分实现代码:
LRESULT CAudioRec::OnStartRecording(WPARAM wp, LPARAM lp)
{
if(recording) return FALSE;
//打开录音设备
MMRESULT mmReturn = ::waveInOpen( &m_hRecord, WAVE_MAPPER,
&m_WaveFormatEx, ::GetCurrentThreadId(), 0, CALLBACK_THREAD);
if(mmReturn!=MMSYSERR_NOERROR)
return FALSE;
if(mmReturn==MMSYSERR_NOERROR)
{
for(int i=0; i < MAXRECBUFFER ; i++)
{
//为录音设备准备缓存
mmReturn = ::waveInPrepareHeader(m_hRecord,rechead[i], sizeof(WAVEHDR));
//给输入设备增加一个缓存
mmReturn = ::waveInAddBuffer(m_hRecord,rechead[i], sizeof(WAVEHDR));
}
mmReturn = ::waveInStart(m_hRecord); //开始录音
if(mmReturn==MMSYSERR_NOERROR )
recording=TRUE;
}
return TRUE;
}
相对录音而言,播放就简单多了,同样用的一个CWinThread对象CAudioPlay来实现,部分实现代码:
LRESULT CAudioPlay::OnWriteSoundData(WPARAM wParam, LPARAM lParam)
{
// TRACE("CAudioPlay::OnWriteSoundData\n");
MMRESULT mmResult = FALSE;
char *p=NULL;
int length=(int) wParam;
if(Playing==FALSE) return FALSE;
if(length<=0) return FALSE;
WAVEHDR *lpHdr=new WAVEHDR;
if(!lpHdr) return FALSE;
p=new char [length];
if(!p)
{
delete lpHdr;
return FALSE;
}
ZeroMemory(lpHdr,sizeof(WAVEHDR));
ZeroMemory(p,length);
CopyMemory(p,(char*)lParam,length);
lpHdr->lpData=p;
lpHdr->dwBufferLength = length;
mmResult = ::waveOutPrepareHeader(m_hPlay, lpHdr, sizeof(WAVEHDR)); //为回放设备准备内存块
if(mmResult)
{
delete lpHdr;delete p;
return mmResult;
}
mmResult = ::waveOutWrite(m_hPlay, lpHdr, sizeof(WAVEHDR));//写数据(放音)
if(mmResult)
{
delete lpHdr;delete p;
return mmResult;
}
m_Count++;
return MMSYSERR_NOERROR;
}
视频采集采用AVICap从视频采集卡捕获视频图像,得到的是位图形式的视频帧,然后用Divx编码器进行压缩,压缩以后形成以帧为格式的Mpeg4流。通过Winsock实现压缩后的视频数据在局域网中的实时传输,接收完的数据交给Divx解码器,以帧的格式解压,最后实现视频显示。所以提出以帧为单位发送视频数据流。为了在接收端能够方便地提取出一帧,提出如表1所示的格式组建帧。完整的一帧由5个字段组成,各个字段的意义如下:帧开始标志:标志着一帧地开始,占用4个字节的空间;帧大小:表示整个帧的大小,包括5个字段的大小,占用4个字节的空间;帧编号:表示帧的顺序编号,占用4个字节的空间;帧类型:标志此帧是否是关键帧,占用1个字节的空间;帧数据:存放压缩后一帧的完整数据。
处理视频传输如下图所示。
相对于视频的传输,语音的传输就简单得多了,在这里建立了两个线程来处理,先来用一个语音录制线程在一个客户端录制语音,再通过用G729a对语音进行编码,然后传输到另一客户端,同样用G729a对语音进行解码,然后用一个语音回放线程将语音播放出来。
将视频语音信息在客户端显示出来,如下图所示。
[1] 谢希仁.计算机网络[M].北京:电子工业出版社
[2] W. Richard Stevens.TCP/IP详解[M].北京:机械工业出版社
[3] 张炯.Unix网络编程[M].北京:清华大学出版