一 . 概述
此模块是在基于 S3C2440 的嵌入式开发板上实现对摄像头采集并经过 H.264 压缩过的数据的 RTP 传输,并在接收端 PC 上能正确接收的功能。
RTP 即实时传输协议,用于 Internet 上针对多媒体数据流的传输。它通常使用 UDP 协议来传送数据,起初是为了“ multicast ” 传输情况而设计的,目的是提供时间信息和保证流同步,不过现在也用于一对一的传输情况。 RTP 协议主要完成对数据包进行编号,加盖时戳,丢包检查,安全与内容认证等工作。通过这些工作,应用程序会利用 RTP 协议的数据信息保证流数据的同步和实时传输。
二 . 流媒体传输系统设计与实现
1. Linux 平台实时流媒体编程
RTP 是目前解决流媒体实时传输问题的最好办法,如果需要在 Linux 平台上进行实时流媒体编程,可以考虑使用一些开放源代码的 RTP库,如 LIBRTP 、 JRTPLIB 等。 JRTPLIB 是一个面向对象的 RTP 库,它完全遵循 RFC 1889 设计,我拟采用 JRTPLIB 实现 RTP/RTCP 协议,下面就以 JRTPLIB 为例,讲述如何在 Linux 平台上运用 RTP 协议进行实时流媒体编程。
1) PC 机上的环境搭建
JRTPLIB 是一个用 C++ 语言实现的 RTP 库,目前已经可以运行在 Windows 、 Linux 、 FreeBSD 、 Solaris 、 Unix 和VxWorks 等多种操作系统上。要为 Linux 系统安装 JRTPLIB ,首先从 JRTPLIB 的网站( http://research.edm.uhasselt.be/~jori/page/index.php?n=CS.Jrtplib )下载最新的源码包,此处使用的是 jrtplib-3.7.1 和jthread-1.2.1( 控制线程库 ) 。假设下载后的源码包保存在/root 目录下,对其进行解压缩,接下去需要对 JRTPLIB 进行配置和编译:
[root@linuxgam src]# cdjrtplib-3.7.1
[root@linuxgam jrtplib-3.7.1]#./configure
[root@linuxgam jrtplib-3.7.1]# make
最后再执行如下命令就可以完成 JRTPLIB 的安装:
[root@linuxgam jrtplib-3.7.1]# makeinstall
再进入 jthread-1.2.1 目录下重复以上操作:
[root@linuxgam src]# cdjthread-1.2.1
[root@linuxgam jrtplib-3.7.1]#./configure
[root@linuxgam jrtplib-3.7.1]# make
[root@linuxgam jrtplib-3.7.1]# makeinstall
生 成的动态库安装到了 /usr/local/lib 目录下
头文件在 /usr/local/include 目录下
2) 嵌入式版的环境搭建
嵌入式版上的环境搭建和 PC 机上有些不同,如不注意可能导致两个库都不能使用。
首先,必须先安装 jthread 库,再安装 jrtplib 库;其次,要交叉编译,需修改 configure 文件。具体步骤为:
[root@linuxgam src]# cdjthread-1.2.1
[root@linuxgam jrtplib-3.7.1]#./configure -host=arm-linux – prefix=/usr/local/arm/2.95.3
[root@linuxgam jrtplib-3.7.1]# make
[root@linuxgam jrtplib-3.7.1]# makeinstall
[root@linuxgam src]# cdjrtplib-3.7.1
[root@linuxgam jrtplib-3.7.1]#./configure -host=arm-linux -prefix=/usr/local/arm/2.95.3 cross_compile=yes
注意:
1. 在 rtperrors.h 中添加头文件 stdio.h ,否则编译会出现相应的问题。
2. 修改 rtpconfig_unix.h 文件(具体方法在调试记录部分有阐述):
注释掉 src/rtpconfig_unix.h (或者 rtpconfig_win.h )这个文件,里面果然定义了一个 RTP_BIG_ENDIAN
3. 修改 rtpsession.cpp 文件如下:
if (!gotlogin)
{
// char *logname =getenv("LOGNAME");
char*logname = "root";
if(logname == 0)
return ERR_RTP_SESSION_CANTGETLOGINNAME;
strncpy((char *)buffer,logname,*bufferlength);
}
然后再运行
[root@linuxgam jrtplib-3.7.1]# makeclean; make
[root@linuxgam jrtplib-3.7.1]# makeinstall
1) 初始化
在使用 JRTPLIB 进行实时流媒体数据传输之前,首先应该生成 RTPSession 类的一个实例来表示此次 RTP 会话,然后调用Create() 方法来对其进行初始化操作。 RTPSession 类的 Create() 方法只有一个参数,用来指明此次 RTP 会话所采用的端口号。
2) 数据发送
当 RTP 会话成功建立起来之后,接下来就可以开始进行流媒体数据的实时传输了。首先需要设置好数据发送的目标地址, RTP 协议允许同一会话存在多个目标地址,这可以通过调用 RTPSession 类的 AddDestination() 、 DeleteDestination() 和ClearDestinations() 方法来完成。目标地址全部指定之后,接着就可以调用 RTPSession 类的 SendPacket() 方法,向所有的目标地址发送流媒体数据。 SendPacket() 最典型的用法是类似于下面的语句,其中第一个参数是要被发送的数据,而第二个参数则指明将要发送数据的长度,再往后依次是 RTP 负载类型、标识和时戳增量。
sess.SendPacket(buffer, 5, 0, false, 10);
对于同一个 RTP 会话来讲,负载类型、标识和时戳增量通常来讲都是相同的, JRTPLIB 允许将它们设置为会话的默认参数,这是通过调用 RTPSession 类的 SetDefaultPayloadType() 、 SetDefaultMark() 和 SetDefaultTimeStampIncrement() 方法来完成的。为 RTP 会话设置这些默认参数的好处是可以简化数据的发送,例如,如果为 RTP 会话设置了默认参数:
sess.SetDefaultPayloadType(0);
sess.SetDefaultMark(false);
sess.SetDefaultTimeStampIncrement(10);
之 后在进行数据发送时只需指明要发送的数据及其长度就可以了:
sess.SendPacket(buffer, 5);
3) 数据接收
对 于流媒体数据的接收端,首先需要调用 PollData() 方法来接收发送过来的 RTP 或者 RTCP 数据报。由于同一个 RTP 会话中允许有多个参与者(源),因此既可以通过调用 GotoFirstSource() 和 GotoNextSource() 方法来遍历所有的源,也可以通过调用GotoFisstSourceWithDat() 和 GotoNextSourceWithData() 方法来遍历那些携带有数据的源。在从 RTP 会话中检测出有效的数据源之后,接下去就可以调用 RTPSession 类的 GetNextPacket() 方法从中抽取 RTP 数据报,当接收到的 RTP 数据报处理完之后,要及时释放。下面的代码示范了该如何对接收到的 RTP 数据报进行处理:
do{
sess.BeginDataAccess();
if (sess.GotoFirstSourceWithData())
{
do
{
RTPPacket *pack;
uint8_t *data;
size_t length;
if((outfile=open("/mnt/hgfs/download/temp1.YUV",O_WRONLY | O_CREAT |O_APPEND))<0)
{
perror("open outfile error");
return -1;
}
while ((pack = sess.GetNextPacket()) != NULL)
{
data=pack->GetPayloadData();
length=pack->GetPayloadLength();
if((write(outfile,data,length))<0)// 将接收到的图片信息写入文中。
{
perror("write outfile error;");
return -1;
}
sess.DeletePacket(pack);
}
} while (sess.GotoNextSourceWithData());
}
sess.EndDataAccess();
close(outfile);
JRTPLIB 为 RTP 数据报定义了 3 种接收模块,通过调用 RTPSession 类的 SetReceiveMode() 方法可以设置下列这些接收模式:
RECEIVEMODE_ALL :缺省的接收模式,所有到达的 RTP 数据报都将被接受;
RECEIVEMODE_IGNORESOME :除了某些特定的发送者之外,所有到达的 RTP 数据报都将被接受,而被拒绝的发送者列表可以通过调用 AddToIgnoreList() , DeleteFromIgnoreList() 和 ClearIgnoreList() 方法来进行设置;
RECEIVEMODE_ACCEPTSOME :除了某些特定的发送者之外,所有到达的 RTP 数据报都将被拒绝,而被接受的发送者列表可以通过调用 AddToAcceptList() , DeleteFromAcceptList 和 ClearAcceptList() 方法来进行设置。
1. Window 平台实时流媒体编程
压缩包可以从这里获得: http://www.bairuitech.com/upimg/soft/jrtplib-3.7.1.rar
下载 jrtplib-3.7.1.rar 后,首先将其解压到一个临时文件夹中,然后开始后续工作。
首先需要强调的是, jrtplib 是一个库而不是应用程序,编译后我们获得的是 .lib 文件。这个文件是用来实现 RTP 协议的,意义和我们在写WIN32 程序时用到的 kernel.lib 一样。
解压后的文件夹中包含两个目录, jrtplib-3.7.1 和 jthread-1.2.1 ,打开这两个目录后我们可以看到下面又有两个同名的目录,为了后面能顺利编译,我们把同名目录下的文件全部考到上一级目录中,就是说把 f:\jrtplib-3.7.1\jrtplib-3.7.1\*.*复制到 f:\jrtplib-3.7.1\ 。同理,把 f:\jthread-1.2.1\jthread-1.2.1\*.* 复制到 f:\jthread-1.2.1\
完成上述步骤后我们就可以 开始编译库文件了。
Windows 平台下建议使用 Visual C++6.0 。
首先编译多线程库 jthread ,在 vc6 中直接打开工作区文件 jthread.dsw ,改变工程设置,选中 source file 下的文件,点右键选择 setting ,确保 code generation 下 Use run-time library 为 debug mulitithreaded DLL 或 debug mulitithreaded 。
然后选 build 就可以了,和上面一样的方法完成 jrtpthread 的编译。这个底下的文件比 jthread 多一些。
默认产生的文件是 jthread.lib 和 jrtplib.Lib, 这两个文件分别位于两个文件夹下的 debug 文件夹下,将它们复制到 VC6的lib 文件夹下。
完成上述工作后我们就可以开始尝试编译 jrtplib 附带的 examples 。
创建一个新的 Win32 Console 应用程序项目,添加 example 文件到 source files 文件夹中,然后添加 jrtplib 工程下的所有.h 头文件,这里我们可以用 VC6 提供的一个功能偷懒:)将 jrtplib 项目添加到本工作区,然后将 Header Files 下的所有文件复制到我们创建的工程的 Header Files 文件夹里面。
修 改 example.cpp 文件,在文件开始添加:
#pragma comment(lib, "jrtplib.lib")
#pragma comment(lib, "jthread.lib")
#pragma comment(lib, "WS2_32.lib")
或者 在 VC 中 a)Project->Settings->Link 中 Object/library modules: 添加 jthread.lib jrtplib.lib ,
b) Link 中添加 ws2_32.lib
检查 code generation ->debugmulitithreaded DLL 或 debug mulitithreaded ,方法同上文中检查库文件的方法。
最后就可以编译、连接、生成可执行文件了。
3 、具体流程图(略)
三 . 调试记录
(1). 输入端口, IP 后出错
ERROR: Can't retrieve login name
这是 rtpsession.cpp 中的 createCNAME 函数有问题
if (!gotlogin)
{
// char *logname =getenv("LOGNAME");
if (logname == 0)
return ERR_RTP_SESSION_CANTGETLOGINNAME;
strncpy((char*)buffer,logname,*bufferlength);
}
logname 要求获得登陆名,而板子一般没有登陆名,将其强制改为 root 即可
if (!gotlogin)
{
// char *logname =getenv("LOGNAME");
char *logname = "root";
if (logname == 0)
return ERR_RTP_SESSION_CANTGETLOGINNAME;
strncpy((char*)buffer,logname,*bufferlength);
}
( 2 )板子和 PC 收发数据不能接收
在 PC 和板子上同时运行 jrtplib 例子程序 example1 (此程序可同时收发),在 PC 和板子之间收发数据,程序能够运行但双方都接收不到数据,结果如下:
查阅资料发现是字节序和位域的问题, 一 般 x86 的 pc 机是用小端字节序 (little endian), 而嵌入式平台一般是大端字节序(big endian), 可能是由于字节序的不同,导致了明明存在数据包,却认不出来的问题。
这是一个位域结构体, jrtplib 库使用哪种字节序完全取决于 RTP_BIG_ENDIAN 的定义,这样问题就简单化了。
看了一下我编译 arm 下 jrtplib 库的 rtpconfig_unix.h (或者 rtpconfig_win.h ) 这个文件,里面果然定义了一个RTP_BIG_ENDIAN ,所以要和 pc 采用的小端字节序一样,先是直接在 rtpconfig_unix.h 中注释掉了
然后在重新编译库,执行
./configure – host=arm-linux – prefix=/usr/local/arm/2.95.3
make
make install
完 了再次运行 example1 ,还是不行,查看 rtpconfig_unix.h 发现刚注释掉到内容又恢复了,
最后查找发现是此文件是由 ./configure 命令生成的,所以 先执行 ./configure 命令,然后再注释上面的内容 ,最后
make
make install
编译完成再次运行 example1 ,能受到数据包,结果如下:
( 3 )自己写的接收程序写文件出错
接收端程 序是在 example3 的基础上修改的,收到到数据包信息全部存在
RTPPacket *pack;
这个类指针当中,可以通过
uint8_t *data;
size_t *length;
data=pack->GetPayloadData();
length=pack->getPayloadLength();
提 取出负载数据和负载长度。
收到数据以后以文件形式存下来。
if((write(outfile,data,length))<0)
{
perror("writeoutfile error;");
return -1;
}
最开始把打开文件放在开头,写入文件放在接收数据之后,但一直不能正确写文件,提示:
bad file descriptor
后 来发现把打开文件放到写文件之前(即在接收数据到 while 循环之内)可以正确写数据,分析原因觉得可能是由于接收程序是一个多线程控制的而引起的。但是这样每次接收都要打开文件,会导致接收速度变慢,试着把打开 文件放到循环外边发现也可以正确写数据,具体是什么原因导致这样暂时还不清楚。
( 4 )接收数据时有数据丢失现象,发送端发送数据时发现发送速度太快,所以数据瞬间发完,而不像例子程序一样一包一包的发送,最开始一直以为是设置 时戳单元 和 时戳增量 有问题,
sessparams.SetOwnTimestampUnit(1.0/1000.0);
sess.SetDefaultTimestampIncrement(10);
但 改了几次还是没有变化,最后仔细对比例子程序,发现是
RTPTime ::Wait(RTPTime(0,0));
这个函数的位置放错 了,此函数的作用就是发完一个包后等待一定时间(其中括号中第一个参数表示秒,第二个表示微秒),发送程序中将其放到了 while 循环之外,没有了这个等待时间而接收端还是以此间隔接收数据当然会丢失数据了,将其挪到循环之内就可以了。
四 . 存在的问题和拟采取的解决方案
现在接收到的数据是以文件的形式存下来的,但是最后想要达到的目的是与 MPlayer 结合起来,使视频采集,压缩后的数据在接收端能够实时的播放出来,现在存在的问题就是如何把收到的数据流传到 MPlayer 中实时播放,下一阶段的工作首先是将MPlayer 的源代码研究清楚,然后再想办法将接收数据实时传给 MPlayer 。