3.3系统软件实现
3.3.1服务器
服务器实现了采集数据然后压缩后进行实时传输,用了三个线程分别实现了视频的采集压缩(线程1),通过TCP协议建立连接(线程2),压缩后视频流的传输(线程3)。服务器应用程序运行后,服务器即创建线程1进行视频采集,线程2处于阻塞状态。一旦有客户端建立连接,则线程2获得客户端IP信息。以此IP信息为参数建立线程3,线程3通过JRTP协议向客户端传递视频流。此后客户端继续处于阻塞状态,直到有新的客户端连接。服务器端的重要的模块包括视频采集模块,视频压缩模块,和网络传输发送模块。
(1)视频采集模块
Linux内核公开支持的OV511等摄像头芯片,但由于较陈旧在市面不容易找到。我们选用LOGITECH的QUICKCAM COOL摄像头并从网上下载摄像头驱动程序qc-usb-0.6.3.tar.gz然后进行解压、编译、安装。
假定已经搭建好嵌入式Linux的开发环境,下面第一步工作就是USB摄像头的安装与驱动。
确定USB摄像头被正常驱动后,下一步就是使用Video4Linux提供的API函数集来编写视频采集程序。
在Linux下,所有外设都被看成是一种特殊的文件,称为设备文件。系统调用是内核和应用程序之间的接口,而设备驱动程序则是内核和外设之间的接口。他完成设备的初始化和释放、对设备文件的各种操作和中断处理等功能,为应用程序屏蔽了外设硬件的细节,使得应用程序可以像普通文件一样对外设进行操作。
Linux系统中的视频子系统Video4Linux为视频应用程序提供了一套统一的API,视频应用程序通过标准的系统调用即可操作各种不同的视频捕获设备。Video4Linux向虚拟文件系统注册视频设备文件,应用程序通过操作视频设备文件实现对视频设备的访问。
Linux下视频采集流程如图1所示
开启视频设备() |
获取设备信息及图像信息() |
初始化窗,颜色模式,帧状态()
|
捕捉视频帧数据() |
关闭视频设备() |
送压缩模块 |
是否中止采集 |
终止
|
开始
|
N |
Y
|
图2 LINUX下视频采集流程图
Video4Linux视频设备数据结构的定义
struct vdIn {
int fd; //文件描述符
char *videodevice ; //视频捕捉接口文件
struct video_mmap vmmap;
struct video_capability videocap;// 包含设备的基本信息(设备名称、支持的最大最小分辨率、信号源信息等)
int mmapsize;
struct video_mbuf videombuf;映射的帧信息,实际是映射到摄像头存储缓冲区的帧信息,包括帧的大小(size),最多支持的帧数(frames)每帧相对基址的偏移(offset)
struct video_picture videopict;//采集图像的各种属性
struct video_window videowin;
struct video_channel videochan;
int cameratype ; //是否能capture,彩色还是黑白,是否 能裁剪等等。 值如VID_TYPE_CAPTURE等
char *cameraname; //设备名称
char bridge[9];
int palette; // available palette
int channel ; //信号源个数
int grabMethod ;
unsigned char *pFramebuffer;//指向内存映射的指针
unsigned char *ptframe[4];//指向压缩后的帧的指针数组
int framelock[4];//
pthread_mutex_t grabmutex;// 视频采集线程和传输线程的互斥信号
int framesizeIn ;// 视频帧的大小
volatile int frame_cour;// 指向压缩后的帧的指针数组下标
int bppIn;// 采集的视频帧的BPP
int hdrwidth;// 采集的视频帧的宽度
int hdrheight;// 采集的视频帧的高度
int formatIn;//采集的视频帧的格式
int signalquit;//停止视频采集的信号
};
在视频采集之前,先要对Video4Linux进行初始化
初始化阶段用ioctl(int fd, ind cmd, …) 函数和设备进行“对话”。Fd是设备的文件描述符,cmd是用户程序对设备的控制命令 ,省略号一般是一个表示类型长度的参数,也可以没有。初始化步骤如下:
1.打开视频:
open (vd->videodevice, O_RDWR))
2. 读video_capability 中信息包括设备名称,支持最大最小分辨率,信号源信息等。调用函数ioctl (vd->fd, VIDIOCGCAP, &(vd->videocap))成功后可读取vd->capability各分量
3.对采集图象的各种属性进行设置,分为两步 首先获取摄象头缓冲区中video_picture中信息调用函数ioctl(vd->fd, VIDIOCGPICT, &(vd->picture));然后改变video_picture中分量的值,为vd->videopict分量赋新值,调用ioctl (vd->fd, VIDIOCSPICT, &vd->videopict)即可实现
4.对图象截取有两种方式:第一种是用read()直接读取数据,第二种是用mmap是把设备文件映射到内存,用内存映射法一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,所以我们选择这种方法。具体做法是
1 获取摄象头存储缓冲区的帧信息调用ioctl (vd->fd, VIDIOCGMBUF, &(vd->videombuf))
2把摄象头对应的设备文件映射到内存区。调用函数vd->pFramebuffer = (unsigned char *) mmap (0, vd->videombuf.size, PROT_READ | PROT_WRITE, MAP_SHARED, vd->fd, 0),成功调用后设备文件内容映射到内存区,
返回的映象内存区指针给vd->pFramebuffer,失败时返回-1。
3修改vd->vmmap中的设置,例如设置图象帧的垂直水平分辨率,彩色显示格式: vd->vmmap.height = vd->hdrheight;
vd->vmmap.width = vd->hdrwidth;
vd->vmmap.format = vd->formatIn;
图象采集可分为单帧采集和连续帧采集,在本系统中采用连续帧采集的方法采集。将vd->videombuf.framese的值赋为2确定采集完毕摄像头帧缓冲区帧数据进行循环的次数。在循环语句中,采集其中的vd->pFramebuffer + vd->videombuf.offsets[vd->vmmap.frame],使用ioctl (vd->fd, VIDIOCMCAPTURE, &(vd->vmmap)函数,若调用成功,则激活设备真正开始一帧图像的截取,是非阻塞的。接着使用ioctl (vd->fd, VIDIOCSYNC,&vd->vmmap.frame) 函数判断该帧图像是否截取完毕,成功返回表示截取完毕,之后就可将采集到的帧进行压缩,然后将压缩后的文件保存到发送缓冲区中。最后修改 vd->vmmap.frame ,vd->frame_cour的值进行下一次采集。
(2)视频压缩模块
对图像帧的编码是通过调用xvidcore-1.1.0函数库的函数实现的,在使用XVID之前要对XVID进行初始化,在初始化过程中,首先对编码器的各项参数即结构体xvid_enc_create中的各成员进行设定,然后调用xvid_encore(NULL, XVID_ENC_CREATE, &xvid_enc_create, NULL)建立编码器,初始化函数如下:
Int enc_init(int use_assembler)
{
int xerr;
xvid_plugin_single_t single; //运算参数
xvid_plugin_2pass1_t rc2pass1;
xvid_plugin_2pass2_t rc2pass2;
xvid_enc_plugin_t plugins[7];
xvid_gbl_init_t xvid_gbl_init; //xvid初始化参数
xvid_enc_create_t xvid_enc_create; //xvid编码参数
/* Set version -- version checking will done by xvidcore */
memset(&xvid_gbl_init, 0, sizeof(xvid_gbl_init));
xvid_gbl_init.version = XVID_VERSION;
xvid_gbl_init.debug = 0; //设置版本号
/* Do we have to enable ASM optimizations ? */
if (use_assembler) {
xvid_gbl_init.cpu_flags = 0;
}
xvid_global(NULL, XVID_GBL_INIT, &xvid_gbl_init, NULL); //初始化
/*------------------------------------------------------------------------
* XviD 编码器初始化
*----------------------------------------------------------------------*/
memset(&xvid_enc_create, 0, sizeof(xvid_enc_create));
xvid_enc_create.version = XVID_VERSION; //设置版本号
xvid_enc_create.width = XDIM; //编码器输入宽度
xvid_enc_create.height = YDIM; //编码器输入高度
xvid_enc_create.profile = XVID_PROFILE_S_L3; //编码的框架级别
/* init plugins */
xvid_enc_create.zones = NULL;
xvid_enc_create.num_zones = 0;
xvid_enc_create.plugins = NULL;
xvid_enc_create.num_plugins = 0;
/* No fancy thread tests */
xvid_enc_create.num_threads = 0;
/* Frame rate - Do some quick float fps = fincr/fbase hack */
if ((ARG_FRAMERATE - (int) ARG_FRAMERATE) < SMALL_EPS) {
xvid_enc_create.fincr = 1;
xvid_enc_create.fbase = (int) ARG_FRAMERATE;
} else {
xvid_enc_create.fincr = FRAMERATE_INCR;
xvid_enc_create.fbase = (int) (FRAMERATE_INCR * ARG_FRAMERATE);
}
if (ARG_MAXKEYINTERVAL > 0) {
xvid_enc_create.max_key_interval = ARG_MAXKEYINTERVAL;
}else {
xvid_enc_create.max_key_interval = (int) ARG_FRAMERATE *10;
}
//关键帧之间的间距
xvid_enc_create.max_bframes = 0; //B帧设置
xvid_enc_create.bquant_ratio = 150;
xvid_enc_create.bquant_offset = 100;
xvid_enc_create.frame_drop_ratio = 0; //编码弃帧率 从0到100
/* Global encoder options */
xvid_enc_create.global = 0;
/* I use a small value here, since will not encode whole movies, but short clips */
xerr = xvid_encore(NULL, XVID_ENC_CREATE, &xvid_enc_create, NULL);/*创建编码器,但创建编码器后编码器并不马上工作,编码器真正工作时是在enc_main函数中调用ret = xvid_encore(enc_handle, XVID_ENC_ENCODE, &xvid_enc_frame, &xvid_enc_stats)时*/
enc_handle = xvid_enc_create.handle;
return (xerr);
}
在编码过程中一般是让编码器自行觉得什么时候产生I帧,但为了提高容错性或者减小网络传输量,会增大或减小I帧的产生频率。I帧的控制由参数通过xvid_enc_create.max_key_interval来绝定,当它设置成-1时,Xvid系统自动选择当前编码是否为I帧或P帧。当网络状况比较良好时(丢包数较少),可以适当减少I帧数量,这样可以提高服务质量。当网络丢包率上升时,就要考虑增加I帧数量,这样可以更快更好地修正、掩盖错误。
XVID编码的主函数为如下:
Int enc_main(unsigned char *image,
unsigned char *bitstream,
int *key,
int *stats_type,
int *stats_quant,
int *stats_length,
int sse[3])
{
int ret;
xvid_enc_frame_t xvid_enc_frame;
xvid_enc_stats_t xvid_enc_stats;
memset(&xvid_enc_frame, 0, sizeof(xvid_enc_frame));
xvid_enc_frame.version = XVID_VERSION; //帧版本号
memset(&xvid_enc_stats, 0, sizeof(xvid_enc_stats));
xvid_enc_stats.version = XVID_VERSION; //编码状态版本号
/* Bind output buffer */
xvid_enc_frame.bitstream = bitstream;
xvid_enc_frame.length = -1;
/* Initialize input image fields */
if (image) {
xvid_enc_frame.input.plane[0] = image;
xvid_enc_frame.input.csp = XVID_CSP_I420; //视频输入格式
xvid_enc_frame.input.stride[0] = XDIM;
} else {
xvid_enc_frame.input.csp = XVID_CSP_NULL;
}
xvid_enc_frame.vol_flags = 0;
xvid_enc_frame.vop_flags = vop_presets[ARG_QUALITY];
/* Frame type -- let core decide for us */
xvid_enc_frame.type = XVID_TYPE_AUTO;//自动决定帧格式
/* Force the right quantizer -- It is internally managed by RC plugins */
xvid_enc_frame.quant = 3;
xvid_enc_frame.motion = motion_presets[ARG_QUALITY];
/* We don't use special matrices */
xvid_enc_frame.quant_intra_matrix = NULL;
xvid_enc_frame.quant_inter_matrix = NULL;
ret = xvid_encore(enc_handle, XVID_ENC_ENCODE, &xvid_enc_frame,
&xvid_enc_stats);//编码并把编码状态存入xvid_enc_stats
*key = (xvid_enc_frame.out_flags & XVID_KEYFRAME);
*stats_type = xvid_enc_stats.type;
*stats_quant = xvid_enc_stats.quant;
*stats_length = xvid_enc_stats.length;
sse[0] = xvid_enc_stats.sse_y;
sse[1] = xvid_enc_stats.sse_u;
sse[2] = xvid_enc_stats.sse_v;
return (ret);
}
(3)网络传输发送模块
流媒体协议分析:
实时传输协议(Real-time Transport Protocol,RTP)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一(Unicast,单播)或者一对多(Multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTP控制协议。RTP是目前解决流媒体实时传输问题的最好办法,在Linux平台上进行实时流媒体编程,我们采用了开放源代码的RTP库JRTPLIB 3.5.2。JRTPLIB是一个面向对象的高度封装后的RTP库,它完全遵循RFC 3550设计,是一个很成熟的RTP库,而且目前仍在维护中。JRTPLIB提供了简单易用的API供程序开发者使用,它使得我们只需关注发送与接收数据,控制部分(RTCP) 由jrtplib内部实现。
RTP数据协议
RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。RTP数据报的头部格式如图3.1所示:
V=2 |
P |
X |
CC |
M |
PT |
序列号 |
时间戳 | ||||||
同步源标识(SSRC) | ||||||
提供源标识(CSRC) |
图3.1 RTP头部格式
其中几个域及其意义如下:
版本号 (V): 标明RTP协议版本号。
补充位(P):如果该位被设置,则在该packet末尾包含了额外的附加信息。
扩展位(X):如果该位被设置,则在固定的头部后存在一个扩展头部。
标记位 (M):: 该位的功能依具体应用的而定。我们将其作为一帧结束的标志。当M=1时,表示一帧的结束,下一个发送或接收的RTP数据包为新的一帧。
CSRC记数(CC):表示CSRC标识的数目。CSRC标识紧跟在RTP固定头部之后,用来表示RTP数据报的来源。
PT值 |
编码标准 |
采样速率(HZ) |
26 |
JPEG |
90000 |
31 |
H.261 |
90000 |
34 |
h.263 |
90000 |
负载类型(PT):标明RTP负载的格式,包括所采用的编码算法、采样频率等。常用的PT值如图3.2所示:
图3.2 常用的负载类型及PT值
序列号:用来为接收方提供探测数据丢失的方法,但如何处理丢失的数据则由应用程序负责,RTP协议本身并不负责数据的重传。
时间戳:记录了负载中第一个字节的采样时间,接收方依据时间戳能够确定数据的到达是否受到了延迟抖动的影响,但具体如何来补偿延迟抖动则由应用程序本身负责。
RTP数据报包含了传输媒体的类型、格式、序列号、时间戳以及是否有附加数据等信息,这些都为实时的流媒体传输提供了相应的基础。RTP中没有连接的概念,它可以建立在底层的面向连接或面向非连接的传输协议之上;RTP也不依赖于特别的网络地址格式,而仅仅只需要底层传输协议支持组帧(Framing)和分段(Segmentation)就足够了;RTP本身不提供任何可靠性机制,这些需要由传输协议或者应用程序本身来保证。RTP一般是在传输协议之上作为具体的应用程序的一部分加以实现的,如图3.3所示:
具体的应用程序 | |
RTP/RTCP | |
UDP |
TCP |
IPv4/IPv6 | |
局域网/广域网 |
图3.3 RTP与其它协议的关系
RTCP控制协议
RTCP控制协议与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进行控制或者对网络状况进行诊断。RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型:
发送端报告(SR):发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端。
接收端报告(RR):接收端是指仅接收但不发送RTP数据报的应用程序或者终端。
源描述(SDES):主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址等,此外还具有向会话成员传达会话控制信息的功能。
通知离开(BYE):主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话。
RTCP数据报携带有服务质量监控的必要信息,能够对服务质量进行动态的调整,并能够对网络拥塞进行有效的控制。由于RTCP数据报采用的是多播方式,因此会话中的所有成员都可以通过RTCP数据报返回的控制信息,来了解其他参与者的当前情况。
一般应用场合下,发送媒体流的应用程序将周期性地产生发送端报告SR,该RTCP数据报含有不同媒体流间的同步信息,以及已经发送的数据报和字节的计数,接收端根据这些信息可以估计出实际的数据传输速率。另一方面,接收端会向所有已知的发送端发送接收端报告RR,该RTCP数据报含有已接收数据报的最大序列号、丢失的数据报数目、延时抖动和时间戳等重要信息,发送端应用根据这些信息可以估计出往返时延,并且可以根据数据报丢失概率和时延抖动情况动态调整发送速率,以改善网络拥塞状况,或者根据网络状况平滑地调整应用程序的服务质量。
环境搭建 :JRTPLIB是一个用C++语言实现的RTP库。为Linux 系统安装JRTPLIB,从JRTPLIB的网站http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html下载最新的源码包jrtplib-3.5.2.tar.bz2。同时为了加入对线程的支持,需要单独下载jthread1.2.0.tar.bz2.。将下载后的源码包保存在/usr/local/src目录下,执行下面的命令对其进行解压缩
bzip2 -dc jrtplib-3.5.2b.tar.bz2 | tar xvf –
接下去对JRTPLIB进行配置和编译:
cd jrtplib-3.5.2
./configure
make
再执行如下命令完成JRTPLIB的安装:
make install
按照此步骤再安装jthread1.2.0。
流媒体编程
用JRTPLIB进行实时流媒体数据传输之前,首先应该生成RTPSession类的一个实例来表示此次RTP会话;
RTPSession session;
RTPSession 类的构造函数中需要一个表明UDP协议类型的参数,是基于Ipv4
还是基于Ipv6,构造函数默认的协议为Ipv4。
在真正创建会话之前还需设置两个参数 :
第一个参数为设置恰当的时戳单元
RTPSessionParams sessionparams;
sessionparams.SetOwnstampUnit(1.0/90000.0);
函数SetOwnstampUnit(1.0/90000.0)的参数1.0/90000.0表示的是以秒为单元的时戳单元。当使用RTP会话传输90000Hz采样的视频数据时,由于时戳每秒钟将递增90000.0,所以时戳单元应该被设置成1/90000.0。如果是音频数据,则设置为1.0/8000.0。
第二个参数为一个指向RTPTransmissionParams实例的指针。当采用IPv4协议时,应使用类RTPUDPv4TransmissionParams,其中要设置的参数为数据传输所用的端口号。
RTPUDPv4TransmissionParams transparams;
transparams.SetPortbase(localportbase);
然后就可以调用RTPSession 的create()函数真正创建会话:
int status=session.Create(sessionparams,&transparams);
如果RTP会话创建过程失败,Create()方法将返回一个负数,通过它虽然可以很容易地判断出函数调用究竟是成功的还是失败,却很难明白出错的原因到底什么。JRTPLIB采用了统一的错误处理机制,它提供的所有函数如果返回负数就表明出现了某种形式的错误,而具体的出错信息则可以通过调用RTPGetErrorString()函数得到。该函数将错误代码作为参数传入,然后返回该错误代码所对应的错误信息。
下一步就是设置发送数据的目标地址和目标端口号:
RTPIPv4ADDRESS addr(desIP,desportbase);
session.AddDestination(addr);
其它需要设置的参数有默认负载类型,是否设标志位,时间戳增量。
session.SetDefaultPayloadType();
session.SetDefaultMark();
session.SetDefaultTimestampIncrement();
真正发送数据是通过调用SendPacket()函数实现的。
int SendPacket(const void *data,size_t len,uint8_t pt,
bool mark,uint32_t timestampinc);
参数data指针指向要发送的数据,数据的长度为len,负载类型为pt, mark为标志位,取值为0或1,可以使用此标志位判断一帧的开始或结束。
时间戳增量timestampinc 用于表示是否是同一帧数据,对于同一帧数据设置同一时间戳。接收端也可以依据时间戳来判断一帧数据的开始或结束。
int SendPacket(const void *data,size_t len,uint8_t pt,
bool mark,uint32_t timestampinc,uint16_t hdrextID,
const void *hdrextdata,size_t numhdrextwords);
此函数用于发送带附加报头的数据帧。hdrextID用于对不同的报头进行编号,
hdrextdata为指向头数据的指针,报头长度为 numhdrextwords。
程序流程框图:
开始发送 |
该帧大于1400字节? |
通过RTP发送数据 |
将该帧拆成几个不大于1400字节的数据包 |
组帧并发送给解码线程
|
接收到RTP包时间戳与上一个相同? |
网络
|
循环接收直到不同时间戳的RTP包 |
通过RTP接收数据 |
N |
N |
Y |
Y |
图3 网络发送接收程序流程图
对程序流程图的说明:
(1)发送端拆帧的算法如下:
If (该数据帧小于1400个字节){
直接用RTPSessio::SendPacket()发送出去;
}
else
{
把该帧拆成1400个字节一个包再发送。对于同一帧数据,采用相同的时间戳来标记,以利于接收端对数据的接收。
}
(2)接收端组帧算法如下:
while(该RTP包的时间戳和上一个RTP包的时间戳相同)
{
说明该RTP包和上一个RTP包属于同一个视频帧的数据。
把接收到的数据保存在缓存中。}
然后把属于同一视频帧的数据组装好,发送给解码线程。采用拆帧方法传输视频数据比直接发送丢包率更低,且实时性更强,传输质量明显提高。
3.3.2客户端
(1)网络传输接收模块
我们在使用Jrtp 库的同时加入了Jthread 的支持,使得具体的数据接收在后台运行,只需改写类RTPSession的成员函数OnPollThreadStep()和ProcessRTPPacket(const RTPSourceData &srcdat,const RTPPacket &rtppack)。由于同一个RTP会话中允许有多个参与者,通过调用RTPSession类的GotoFirstSourceWithData()和GotoNextSourceWithData()方法来遍历那些携带有数据的源。在函数OnPollThreadStep()中,为了正确接收同一数据源的数据据,必须先对数据源表加锁。通过调用BeginDataAccess()实现,当正确接收一个数据报后,调用EndDataAccess()实现对数据源表的解锁。
在函数ProcessRTPPacket (const RTPSourceData & srcdat,const RTPPacket & rtppack);
中对接收到的数据包进行处理。
首先调用
char * payloadpointer = (char *)rtppack.GetPayloadData ()得到源数据包的指针,并通过rtppack.HasMarker ()来判断是否是一帧的结束。由于一帧数据要分成多个数据包进行传送,需要将收到的包暂时保存到内存缓冲区中,等到有足够的帧数据后再调用解码线程进行解码。memcpy(receivepointer,payloadpointer,rtppack.GetPayloadLength ())将数据暂时保存在receivepointer指向的内存缓冲区中。如果一帧数据结束则设置标志位并作相应处理。
JRTPLIB为RTP数据报定义了三种接收模式,其中每种接收模式都具体规定了哪些到达的RTP数据报将会被接受,而哪些到达的RTP数据报将会被拒绝。通过调用RTPSession类的SetReceiveMode()方法可以设置下列这些接收模式:RECEIVEMODE_ALL:为缺省的接收模式,所有到达的RTP数据报都将被接收;
RECEIVEMODE_IGNORESOME:除了某些特定的发送者之外,所有到达的RTP数据报都将被接收,而被拒绝的发送者列表可以通过调AddToIgnoreList()、DeleteFromIgnoreList()和ClearIgnoreList()方法来进行设置; RECEIVEMODE_ACCEPTSOME:除了某些特定的发送者之外,所有到达的RTP数据报都将被拒绝,而被接受的发送者列表可以通过调用AddToAcceptList ()、DeleteFromAcceptList和ClearAcceptList ()方法来进行设置。
(2)视频解码模块
客户端解码的流程图如下所示:
设置解码帧缓冲区 |
停止显示 |
解码一帧 |
是VOL头 |
Y |
N |
Y |
N |
SDL显示 |
释放解码缓冲区 |
终止 |
开始 |
Dec_init()解码初始化 |
当客户端接收到发送来的压缩后的视频信息后,调用视频解码模块将码流解码成可以播放的YUV格式。在调用解码模块之前要正确的初始化解码器。初始化解码器的程序如下所示:
static int
dec_init(int use_assembler, int debug_level)
{
int ret;
xvid_gbl_init_t xvid_gbl_init;
xvid_dec_create_t xvid_dec_create;
memset(&xvid_gbl_init, 0, sizeof(xvid_gbl_init_t));
memset(&xvid_dec_create, 0, sizeof(xvid_dec_create_t));
/*------------------------------------------------------------------------
* XviD 核心初始化
*----------------------------------------------------------------------*/
/* Version */
xvid_gbl_init.version = XVID_VERSION;//版本号
/* Assembly setting */
if(use_assembler)
xvid_gbl_init.cpu_flags = 0;
else
xvid_gbl_init.cpu_flags = XVID_CPU_FORCE;
xvid_gbl_init.debug = debug_level;
xvid_global(NULL, 0, &xvid_gbl_init, NULL);
/*------------------------------------------------------------------------
* XviD 解码器初始化
*----------------------------------------------------------------------*/
xvid_dec_create.version = XVID_VERSION;//解码器版本号
xvid_dec_create.width = 0;// 帧的宽 自动适应
xvid_dec_create.height = 0;// 帧的高 自动适应
ret = xvid_decore(NULL, XVID_DEC_CREATE, &xvid_dec_create, NULL);
//创建解码实例
dec_handle = (int *)xvid_dec_create.handle; //传递句柄
return(ret);
}
解码过程调用xvid_decore(dec_handle, XVID_DEC_DECODE, &xvid_dec_frame, xvid_dec_stats)将得到的解码状态放入xvid_dec_stats中,根据xvid_dec_stats再对解码后的帧进行处理,解码的主程序如下:
static int dec_main(unsigned char *istream,unsigned char *ostream, int istream_size,xvid_dec_stats_t *xvid_dec_stats)
{
int ret;
xvid_dec_frame_t xvid_dec_frame;
/* Reset all structures */
memset(&xvid_dec_frame, 0, sizeof(xvid_dec_frame_t));
memset(xvid_dec_stats, 0, sizeof(xvid_dec_stats_t));
/* Set version */
xvid_dec_frame.version = XVID_VERSION; // 帧版本号
xvid_dec_stats->version = XVID_VERSION; //解码状态版本号
/* No general flags to set */
xvid_dec_frame.general = 0;
/* Input stream */
xvid_dec_frame.bitstream = istream; //指向输入的视频流的指针
xvid_dec_frame.length = istream_size;//输入视频流的大小
/* Output frame structure */
xvid_dec_frame.output.plane[0] = ostream; //指向输出的视频流的指针
xvid_dec_frame.output.stride[0] = XDIM*BPP;
xvid_dec_frame.output.csp = CSP;//视频输出格式
ret = xvid_decore(dec_handle, XVID_DEC_DECODE, &xvid_dec_frame, xvid_dec_stats);
return(ret);
}
(3)视频显示模块
在本系统中解码得到的YUV格式的视频流用SDL显示,显示的流程图如下所示:
判断帧长宽变化 |
重建YUVOverlay |
Y |
N |
将帧信息拷贝到显示缓冲区 |
锁定SDL |
解锁SDL |
接收一帧并解压 |
显示 |
检测键盘信息QUIT?
|
N |
Y |
初始化SDL
|
结束,调用SDLQUIT()
|
SDL的初始化过程如下:
首先调用SDL_Init (SDL_INIT_VIDEO) 初始化SDL的视频显示系统,然后调用函数SDL_SetVideoMode (320, 240, 0, SDL_HWSURFACE | SDL_DOUBLEBUF| SDL_ANYFORMAT| SDL_RESIZABLE)设置视频模式,包括长,宽,和 BPP,参数SDL_HWSURFACE建立一个带视频存储的SURFACE,SDL_DOUBLEBUF使能双缓冲区,消除Bpp对SURFACE的影响,SDL_RESIZABLE使窗口大小可调整。
最后调用函数SDL_CreateYUVOverlay(XDIM, YDIM, SDL_YV12_OVERLAY, screen)建立一个长为XDIM宽为YDIM,格式为SDL_YV12_OVERLAY的YUV平面,返回一个指向新建SDL_overlay的指针。SDL_overlay结构体如下:
typedef struct SDL_Overlay {
Uint32 format; /* Read-only */
int w, h; /* Read-only */
int planes; /* Read-only */
Uint16 *pitches; /* Read-only */
Uint8 **pixels; /* Read-write */
/* Hardware-specific surface info */
struct private_yuvhwfuncs *hwfuncs;
struct private_yuvhwdata *hwdata;
/* Special flags */
Uint32 hw_overlay :1; /* Flag: This overlay hardware accelerated? */
Uint32 UnusedBits :31;
} SDL_Overlay;
SDL_DisplayYUVOverlay(overlay, &rect) 这就是要显示YUV的具体函数,第一个参数是已经创建的YUV平面,第二个参数是一个矩形宽,设置在显示平面的哪个区域内显示。在使用前,需要将要显示的数据指针送给pixels
outy = out_buffer;
outu = out_buffer+XDIM*YDIM;
outv = out_buffer+XDIM*YDIM*5/4;
for(y=0;y<screen->h && y<overlay->h;y++)
{
op[0]=overlay->pixels[0]+overlay->pitches[0]*y;
op[1]=overlay->pixels[1]+overlay->pitches[1]*(y/2);
op[2]=overlay->pixels[2]+overlay->pitches[2]*(y/2);
memcpy(op[0],outy+y*XDIM,XDIM);
if(y%2 == 0)
{
memcpy(op[1],outu+XDIM/2*y/2,XDIM/2);
memcpy(op[2],outv+XDIM/2*y/2,XDIM/2);
}
}
pitches是指YUV存储数据对应的stride(步长)
四 系统测试
测试环境 局域网
延迟
测试系统延时采用网络时间服务器(NTP),在视频传输之前从网络时间服务器上获得时间,在客户端接收到视频播放之前再一次获得时间,计算两次差值从而得到延时,多次测试得到,在局域网的条件下,延时小于0.1秒。
图象质量:在局域网的条件下,图象无失真