文件夹
1原创性声明----------------------------------------------------3
2 摘要----------------------------------------------------------4
3系统方案------------------------------------------------------4
3.1功能与指标----------------------------------------------4
3.2方案选择与论证------------------------------------------4
系统组成框图--------------------------------------------4
硬件平台介绍------------------------------------------------------------------4
视频採集方案的选择---------------------------------------------------------4
视频编码方案的选择---------------------------------------------------------5
视频传输方案的选择---------------------------------------------------------6
显示方案选择------------------------------------------------------------------6
3.4系统软件实现--------------------------------------------6
3.4.1server--------------------------------------------6
(1)视频採集模块-------------------------------------6
(2)视频压缩模块------------------------------------10
(3)网络传输发送模块---------------------------------13
3.4.2client--------------------------------------------19
(1)网络传输接收模块--------------------------------19
(2)视频解码模块------------------------------------19
(3)视频显示模块------------------------------------22
四 系统測试----------------------------------------------------25
附录:源码
參考书目
2006年英特尔杯大学生电子设计竞赛嵌入式系统专题邀请赛
參赛作品原创性声明
本人郑重声明:所呈交的參赛作品报告,是本人和队友独立进行研究工作所取得的成果。除文中已经注明引用的内容外,本论文不包括不论什么其它个人或集体已经发表或撰写过的作品成果,不侵犯不论什么第三方的知识产权或其它权利。本人全然意识到本声明的法律结果由本人承担。
參赛队员签名:
日期: 年 月 日
2 摘要
本系统在LINUX平台下实现了视频的採集、压缩、传输及组播,图象清晰,实时性较好。本设计採用USB摄像头结合LINUX下自带的驱动模块VIDEO4LINUX实现视频採集。在XVID视频编解码平台下实现视频的压缩和解压。视频传输採用专门为流媒体传输设计的RTP协议,达到了较高的实时性。
ABSTRACT
On the basis of Linux platform, this system realizes the videodata‘s collection, compression and network transmission. The videodata‘s collection is realized through USB camera and Video4Linux. The videodata‘s coding and decoding is realized under the Xvid platform. And the network transmission is realized by Rtp protocal which is designed for streammedia. All of these make hign real-time performance.
关键词:视频 RTP XVID SDL
3 系统方案
3.1实现功能与指标
本系统可用于足球赛场向场内或场外观众提供更逼真的更精彩的比赛画面,使场内观众能够零距离的观看射门等精彩画面。用户可用笔记本电脑由局域网连接server,执行client软件就可以赞赏近距离的比赛画面。server由USB摄像头採集数字视频信息,经过MPEG4视频编码,然后通过JRTP网络传输协议向连接到server的client传输视频信息,实现视频的实时组播。採集到的YUV图像大约为100KB压缩后每帧图像大小平均为5KB,在局域网环境下延迟小于0.5秒,视频清晰无失真。鉴于server的主频限制,组播最大连接数为5,可同一时候向5个用户提供视频信息。
3.2方案选择与论证
系统组成框图:
USB摄像头 |
基于GENE8310的server 10 |
网络 |
远程登录主机 |
硬件平台介绍 :
GENE-8310是AAEON提供的第三代无风扇解决方式,在低功耗情况下能够获取更高的性能表现,主要表如今:卓越性能与可控的功耗,多种显示模式,可扩展性,GENE8310主频为500M 能够做视频採集处理与传输的server。
视频採集方案的选择:
LINUX有自带的摄像头驱动模块Video4Linux. Video4Linux为针对视频设备的应用程序编程提供一系列的接口函数,对于USB摄像头,其驱动程序中须要提供主要的I/O操作接口函数 如open close的实现 以及内存影射功能和对I/O操作的控制接口函数ioctl等。LINUX下视频採集例如以下所看到的
视频应用程序 |
Video4Linux |
设备驱动程序 |
视频採集设备 |
视频编解码方案的选择
Xvid作为第二代MPEG-4编码具有多方面的长处,XVID是DIVX开发小组因不满DIVX被封闭而在其基础上开发的源代码开放的视频编码解码平台。对于第二代的MPEG4视频编码内核来说。XVID的各种特点都有代表性和先进意义。1.它支持多种编码模式:除了最原始的单重估定码流压缩(1-pass CBR)之外,XVID提供了包含:单重质量模式动态码流压缩单重量化(Quantization)模式动态码流压缩、和包含外部控制和内部控制的两种双重(2-pass)动态码流压缩模式。2.在量化方式上Xvid不仅提供了标准的MPEG量化方式,还特地提供了更适合低码流压缩的.h 263量化方式。3.除了量化方式迭择,Xvid还提供了强大的对压缩过程中的量化幅度的范围控制。用户能够选定压缩时同意使用的量化幅度范围。比如设定一个量化的上限,就能够避免可能出现的画质大幅下降的情况。4.在运动侦測(Motion Search)和曲线平衡分配(Curve)方面,XVID对画面帧进行运动侦測以及对全片段的运动侦測结果进行分析后,又一次以曲线平衡分配每一帧的量化幅度,以做到:须要高码流的运动画面能够分配很多其它空间、更高的码流、更低的量化幅度来保持画面的细节;而对于不包含太多运动信息的静态画面,则消减分配预算。这样的把好钢用在刀刃上的做法,是Xvid作为第二代MPEG-4编码的核心内容。5.Xvid提供了多极运动侦測精度,包含半像素插值的技术以16x16像素的微区块为单元标示上运动矢量:以及4分运动矢量(inter4v motionvectors)的方式,以8x8的像素区块为单元更仔细的纪录运动向量以供二重分析。6.动态关键帧距是还有一个Xvid所具有的,在空间和画面之间获得最大平衡的技术。我们知道在视频压缩中不是每一帧都记录着所有的画面信息,其实唯独关键帧记录着完整的画面信息,而兴许的P帧(P-Frame)不过纪录下与之前一帧的差值。假设关键帧之间的画面变化非常大,则会浪费宝贵的空间在P-Frame上;而增加把变化非常大的那一帧记录在关键帧里,那么因为兴许的帧不再有更大的变化,就能够节省P帧所需的空间。因此,依据画面镜头切换和运动幅度来变换关键帧的位置,对于视频压缩下的画面质量提高,就有着事半功倍的效果。
鉴于XVID以上种种长处,我们採用XVID实现视频的编解码。
视频传输方案的选择
视频传输能够选择TCP与UDP,TCP是一个面向连接协议,传输信息前须要建立连接,系统资源开销大,但可靠性较高。UDP是一个无连接协议,数据传输之前源端和终端不须要建立连接,资源开销小,实时性较高。
实时传输协议(Real-time Transport Protocol,RTP)是在Internet上处理多媒体数据流的一种网络协议,利用它可以在一对一(Unicast,单播)或者一对多(Multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,具有UDP传输的长处。
鉴于可靠性考虑,在本系统中,信息传输之前server和client用TCP建立连接。然后server通过RTP向client发送视频信息,这样就达到了可靠性和实时性的平衡。
显示方案选择
SDL(Simple DirectMedia Layer)是一个跨平台的多媒体游戏支持库。当中包括了对图形、声音、线程等等的支持,眼下能够执行在很多平台上,当中包括 X Window、X Window with DGA、Linux FrameBuffer 控制台等等。
由于 SDL 专门为游戏和多媒体应用而设计开发,所以它对图形的支持很优秀,尤其是高级图形能力,比方 Alpha 混和、透明处理、YUV 覆盖、Gamma 校正等等。并且在 SDL 环境中可以很方便地载入支持 OpenGL 的 Mesa 库,从而提供对二维和三维图形的支持。
本系统client接受到的视频解压后为YUV格式,考虑到SDL在YUV覆盖方面的优势,我们选择SDL实现视频信息接收接压后的显示。
3.3系统软件实现
3.3.1server
server实现了採集数据然后压缩后进行实时传输,用了三个线程分别实现了视频的採集压缩(线程1),通过TCP协议建立连接(线程2),压缩后视频流的传输(线程3)。server应用程序执行后,server即创建线程1进行视频採集,线程2处于堵塞状态。一旦有client建立连接,则线程2获得clientIP信息。以此IP信息为參数建立线程3,线程3通过JRTP协议向client传递视频流。此后client继续处于堵塞状态,直到有新的client连接。server端的重要的模块包含视频採集模块,视频压缩模块,和网络传输发送模块。
(1)视频採集模块
Linux内核公开支持的OV511等摄像头芯片,但因为较陈旧在市面不easy找到。我们选用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会话时将同一时候占用两个port,分别供RTP和RTCP使用。RTP本身并不能为按序数据传输包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完毕。通常RTCP会採用与RTP同样的分发机制,向会话中的全部成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话參与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而可以对服务质量进行控制或者对网络状况进行诊断。RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有例如以下几种类型:
发送端报告(SR):发送端是指发出RTP数据报的应用程序或者终端,发送端同一时候也能够是接收端。
接收端报告(RR):接收端是指仅接收但不发送RTP数据报的应用程序或者终端。
源描写叙述(SDES):主要功能是作为会话成员有关标识信息的载体,如username、邮件地址等,此外还具有向会话成员传达会话控制信息的功能。
通知离开(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()方法将返回一个负数,通过它尽管能够非常easy地推断出函数调用究竟是成功的还是失败,却非常难明确出错的原因究竟什么。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.2client
(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)视频解码模块
client解码的流程图例如以下所看到的:
设置解码帧缓冲区 |
停止显示 |
解码一帧 |
是VOL头 |
Y |
N |
Y |
N |
SDL显示 |
释放解码缓冲区 |
终止 |
開始 |
Dec_init()解码初始化 |
当client接收到发送来的压缩后的视频信息后,调用视频解码模块将码流解码成能够播放的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(步长)
四 系统測试
測试环境 局域网
延迟
測试系统延时採用网络时间server(NTP),在视频传输之前从网络时间server上获得时间,在client接收到视频播放之前再一次获得时间,计算两次差值从而得到延时,多次測试得到,在局域网的条件下,延时小于0.1秒。
图象质量:在局域网的条件下,图象无失真。