1 硬件系统结构
整个系统主要是在Intel PXA255评估平台Sitsang板上实现的。PXA255是基于Intel的Xscale架构的嵌入式处理器,该平台配备了大量的硬件资源。整个系统的硬件结构如图1所示。
2 软件系统结构
系统中采用的是基于Qt/Embedded 2.3.10版本的嵌入式图形库。该图形库是基于Linux系统的Frame Buffer机制的,并使用基于该图形库的Qtopia 2.2.1 PDA版本的窗口环境管理系统。Qt/Embedded是一个完整的自包含GUI和基于Linux的嵌入式平台开发工具,是QT的嵌入式开发版本。
音频和视频信号的采集、压缩、播放和传输都是建立在该图形界面和嵌入式Linux内核以上的,所以,在交叉编译移植嵌入式Linux内核时,要正确配置对USB、Video4Linux、摄像头和音频设备的支持以及对Frame Buffer机制的支持。交叉编译嵌入式QT时,要配置使其支持多线程、JPEG算法库、音频设备以及qvfb(基于X11的虚拟Frame Buffer机制)。本文主要讨论IP视频电话系统的设计实现,故嵌入式Linux内核和嵌入式QT的配置编译过程不再详述。整个系统的软件结构如图2所示。
3 系统的具体设计
本IP视频电话系统主要由音频采集/播放模块、视频采集/播放模块和网络传输模块组成。音频和视频模块采样本地数据,压缩处理后交给网络传输模块,由其发送到另一对话端,并从网络传输模块接收对方的音频和视频数据处理后进行播放。
3.1 网络传输模块设计
系统启动后,本地服务器端即对5000端口进行监听。若有IP电话连接进来,则接受连接,为其分配套接字资源,并根据通话类型,生成相应的音频、视频类实例来处理相应的音频、视频数据。系统可以实现视频通话,也可以只进行语音通话,即实现传统IP电话的功能,因为音频、视频数据格式不同,需要分别做不同的处理,故采用两个不同的套接字来进行处理,网络传输模块服务器端的基本流程如图3所示。
本地网络服务器端用从QServerSocket类继承的子类IPphoneServer实现。QT/Embedded类库已经对网络操作进行了很好的封装,所以系统只利用QT的信号和槽机制,给IPphoneServer类增加一个新的信号--VoidnewConnect(int)。信号所带的参数为套接字号,并重载了QServerSocket的虚子函数成员void newConnect(intsocket)。一旦发现有新的连接,它将把服务器端给新连接指定的接收套接字号(int socket)通过信号newConnect(int)发送出来,以便主程序设置对应的数据处理套接字。
在系统主程序中启动服务器,并将服务器实例的newConnect(int)信号连接到主程序的newConnect(int)槽函数上。一旦来了新的连接,server的newConnect(int)被发出,则由主程序的newConnect(int)槽函数接收套接字号并判断是音频还是视频连接后,将其指定给相应的音频或视频数据传输套接字,启动服务器的代码如下:
server=new IPphoneServer((tPort->text()).toUShort());connect(server,SIGNAL(newConnect(int)),SLOT(new-Connect(int)));
具体与音频/视频模块相关连进行数据传输的套接字从QSocket类继承来的子类IPPDataSock实现,它增加了一个新的QDataStream类指针成员变量ds来进行数据的传输处理,在IPPDataSock的构造函数中被实例化。
为音频和视频进行数据传输的套接字实例分别为aDataSock和vDataSock。若从已方发起连接,先通过QT的信号和槽机制设定相应的套接字连接、关闭和其他处理槽函数,再使用connectToHost()函数连接到远端服务器即可。音频套接字实例化代码如下:
aDataSock=new IPPDataSock(this); connect(aDataSock,SIGNAL(connected()),SLOT(tConnected())); connect(aDataSock,SIGNAL(connectionClosed()),SLOT(tClosed())); connect(aDataSock,SIGNAL(readyRead()),IPAudio,SLOT(canPlay())); connect(aDataSock,SIGNAL(error(int)),SLOT(tError(int))); aDataSock->connectToHost(tServer->text(),(tPort->text()).toUShort());
3.2 音频采集/播放模块设计
音频采集/播放模块主要是实现IP电话的音频处理,由自定义类IPAudio来实现,因为系统要同时发送本地音频数据给对话端并接收来自对话端的音频数据在本地播放,而只有一个音频编解码设备,所以音频设备必须以全双工方式工作,音频采集/播放模块的主要工作流程如图4所示。
系统采用的是Linux操作系统,其下的音频编程遵循OSS(Open Sound System)音频接口标准,OSS是Unix/Linux平台上统一的音频接口,只要音频处理应用程序按照OSS的API来编写,它就可以提供源代码级的可移植性。
Linux下的设备全部使用设备文件来管理,本系统使用的数字音频设备为/dev/dsp。可以播放或录制数字化的声音,读这个设备就相当于录音,写这个设备就相当于放音,它使用8位(无符号)线性编码,其主要指标参数有:采样速率(电话为8Kbps)、声道数目(单声道、立体声)和采样分辨率(8位、16位)。
在进行音频的采集和播放之前,必须先打开该音频设备并适当设置一些工作参数,这些都在IP Audio类的构造函数中实现,其中的一些参数和操作都被定义在"soundcard.h"头文件中。
首先,要打开音频设备。因为系统在通话时要同时进行录音和放音,所以使用读写模式,相关代码片断如下:
int audio_fd;
if((audio_fd=open("/dev/dsp",O_RDWR))<0) …//错误处理
打开设备后,为了正常地工作,设置一些相应的工作参数。
1)先设置为全双工工作模式,并检查是否设置成功,代码如下:
设置好各个参数后,就可以进行视频的采集和播放了,采集及录音使用OSS提供的read()函数,播放则使用对应的write()函数,直接对音频设备/dev/dsp进行操作,由于进行IP电话通话时,要进行不间断录音和放音,但音频设备的输入/输出缓冲区的大小是有限的,必须不断循环使用,因此采用QT/Embedded的信号和槽机制来实现,系统采集完一次数据并发送出去后,给IPAudio类自身发送一个canRecord()信号,而采集函数本身是一个槽,接收到canPlay()信号后又开始下一次采集。这样循环不断,代码片断如下:
public slots; void record(){ int len; if(ioctl(audio_fd,SOUND_PCM_SYNC,0)==-1) //同步 … //错误处理 if(len=read(audio_fd,buf,1024))!=1024) //录音 printf("Read wrong number of bytes %d ",len); else{ (*(aDataSock->ds))<<buf; //发送采集的音频数据给对话端 emit canRecord();//发送可录音信号 } }
当系统接收到对话端发送过来的音频数据时,音频接收套接字aDataSock发送一个readyRead()数据已准备好的信号给IPAudio类的槽函数play()来播放这段音频。套接字aDataSock初始化时的语句
connect(aDatasock,SIGNAL(readyRead()),IPAudio,SLOT(play()));
即实现这个功能,播放时为了避免要播放的数据太多而导致设备被阻塞,还须先检测音频设备的输出缓冲区是否有足够的空间,以使系统能够正常工作。代码如下:
这样,系统就可以实现无阻塞的音频采集和播放,一个传统的IP语音电话就实现了,系统退出时,用close()函数关闭音频设备即可。
3.3 视频采集/播放模块设计
视频采集和播放模块实现了通过摄像头采集本端影像视频传输给对话方并接收对方的视频数据还原成影像显示在本端屏幕上的功能,也是本IP电话系统的先进之处,程序中用多的一些Video4Linux支持的专用视频数据结构如下:
1)video_capability,包含摄像头的基本信息,如设备名称、支持的最大最小分辨率、信号源信息等,分别对应着结构体中成员变量name[32]、maxwidth、maxheight、minwidth、minheight、channels(信号源个数)、type等;
2)video_picture,包含设备采集图像的各种属性,如brightness(亮度)、hue(色调)、contrast(对比度)、whiteness(色度)、depth(深度)等;
3)video_mmap,用于内存映射;
4)video_mbuf,利用mmap进行映射的帧信息,实际上是输入到摄像头存储器缓冲中的帧信息,包括size(帧的大小)、frames(最大支持的帧数)、offsets(每帧相对基址的偏移);
5)video_Window,包括设备采集窗口的各种参数。
视频采集/播放模块的基本工作流程如图5所示。
系统使用从QWidget继承而来的IPVideo类进行视频数据的处理,在采集和播放之前,必须先对视频设备初始化,正确配置一些工作参数,打开视频设备仍然使用open()函数,设备文件名为/dev/video0,在构造函数中完成并对函数设备初始化,初始化是通过读取摄像头的一些信息来设置设备采集窗口的大小,如下:
struct video_capability cap; struct video_window win; if(ioctl(video_fd,VIDIOCGCAP,&cap)==-1) //读取摄像头信息 …//错误处理 w=win.width=cap.maxwidth; h=win.height=cap.maxheight; frameSize=w*h; if(ioctl(video_fd,VIDIOCSWIN,&win)==-1) //设置采集窗口大小 …//错误处理
进行初始化设备工作后,就可以对视频图像进行采集了,通常有两种方法:一种是使用read()直接读取视频数据;另外一种是通过mmap()内存映射来实现,read()通过内核缓冲区来读取数据,而mmap()通过把设备文件映射到内存中,绕过了内核缓冲区,加速了I/O访问,显然比使用read()函数快。所以在系统实现中采用mmap()内存映射方式。
利用mmap()方式对视频进行采集时,先获得摄像头存储缓冲区的帧信息,之后修改video_mmap中的设置,可以重新设置图像帧的重新及水平分辨率、彩色显示格式,接着把摄像头对应的设备文件映射到内存区,代码片断如下:
这样摄像头设备所采集的内容就映射到了内存缓冲区pixBuf中,该映射内容区可读可写并可与其他进程共享。将系统设置为单帧采集模式,当1帧数据采集完毕时,通过vDataSock套接字将视频数据传送给对方,然后发一个canSample()信号给自身再开始下一帧数据的采集,如下:
在采集视频数据的同时,还要显示对方传输过来的视频数据,当对方的数据被接收到时,系统利用vDataSock的readyRead()信号告诉IPVideo将其显示出来。IPVideo使用QT/Embedded的QImage和QPainter类来实现图像数据的显示,先初始化,为了使图像重画时不闪烁,设置WRepaintNoErase重画不擦除标志,如下:
p=new QPainter(); image=new QImage((uchar*)buff,w,h,32,0,0,(QImage::Endian)0); setWFlags(getWFlags()|Qt::WRepaintNoErase);
重载IPVideo的paintEvent()函数,加载buff中接收过来的视频数据,并在屏幕上画出来,代码如下:
void paintEvent(QPaintEvent*){ image->loadFromData((uchar*)buff,frameSize); p->begin(this); p->drawImage(0,0,*image); p->end(); }
在IPVideo中增加一个槽函数show(),专门接收vDataSock的readyRead()信号,一旦接收到了,就通过vDataSock的ds将视频数据流导入buff缓冲区中,并调用updata()函数,该函数将激活paint事件,调用paintEvent()函数进行视频的更新重画。这样,随着不停地接收到对方的图像数据,就实现了远端视频的播放,双方就能进行语音和视频同步的IP通话了。
4 小结
本系统主要是针对嵌入式手持设备,可与PC或同类型的手持机进行IP视频电话通信,扩展了传统IP电话的功能,弥补了没有图像的缺点,并且体积小、携带方便、全图形界面,操作简单,采用无线上网,只要网络支持,可以随时随地使用,另外还可以做终端监控之用,可以固定也可以移动监控,广泛地应用于工厂、银行及小区等众多场合,具有比较广阔的市场和应用前景。