您的位置:首页 > 运维架构 > Linux

基于Linux的视频传输系统(转)

2010-08-20 10:54 357 查看
目录



1



原创性声明----------------------------------------------------3



2



摘要----------------------------------------------------------4



3



系统方案------------------------------------------------------4



3.1



功能与指标----------------------------------------------4



3.2



方案选择与论证------------------------------------------4



系统组成框图--------------------------------------------4



硬件平台介绍



------------------------------------------------------------------4



视频采集方案的选择



---------------------------------------------------------4



视频编码方案的选择



---------------------------------------------------------5



视频传输方案的选择



---------------------------------------------------------6



显示方案选择



------------------------------------------------------------------6



3.4



系统软件实现--------------------------------------------6



3.4.1



服务器--------------------------------------------6



(1

)视频采集模块-------------------------------------6



(2

)视频压缩模块------------------------------------10



(3

)网络传输发送模块---------------------------------13



3.4.2



客户端--------------------------------------------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



实现功能与指标



本系统可用于足球赛场向场内或场外观众提供更逼真的更精彩的比赛画面,使场内观众可以零距离的观看射门等精彩画面。用户可用笔记本电脑由局域网连接服务器,运行客户端软件即可欣赏近距离的比赛画面。服务器由

USB

摄像头采集数字视频信息,经过

MPEG4

视频编码,然后通过

JRTP

网络传输协议向连接到服务器的客户端传输视频信息,实现视频的实时组播。采集到的

YUV

图像大约为

100KB

压缩后每

帧图像大小平均为5KB



在局域网环境下延迟小于

0.5

秒,视频清晰无失真。鉴于服务器的主频限制,组播最大连接数为

5

,可同时向

5

个用户提供视频信息。

3.2



方案选择与论证



系统组成框图:



USB摄像头

基于GENE83

10

的服务器

10

网络

远程登录主机

硬件平台介绍







GENE-8310



AAEON

提供的第三代无风扇解决方案,在低功耗情况下可以获取更高的性能表现,主要表现在:卓越性能与可控的功耗,多种显示模式,可扩展性,

GENE8310

主频为

500M

可以做视频采集处理与传输的服务器。

视频采集方案的选择:



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

传输的优点。

鉴于可靠性考虑,在本系统中,信息传输之前服务器和客户端用TCP

建立连接。然后服务器通过RTP

向客户端发送视频信息,这样就达到了可靠性和实时性的平衡。

显示方案选择



SDL



Simple DirectMedia Layer

)是一个跨平台的多媒体游戏支持库。其中包含了对图形、声音、线程等等的支持,目前可以运行在许多平台上,其中包括

X Window



X Window with DGA



Linux FrameBuffer

控制台等等。

因为

SDL

专门为游戏和多媒体应用而设计开发,所以它对图形的支持非常优秀,尤其是高级图形能力,比如

Alpha

混和、透明处理、

YUV

覆盖、

Gamma

校正等等。而且在

SDL

环境中能够非常方便地加载支持

OpenGL



Mesa

库,从而提供对二维和三维图形的支持。

本系统客户端接受到的视频解压后为

YUV

格式,考虑到

SDL



YUV

覆盖方面的优势,我们选择

SDL

实现视频信息接收接压后的显示。

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

接收数据









             图

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

秒。

图象质量:



在局域网的条件下,图象无失真
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: