目 录
一. 概述
二. 流媒体传输系统设计与实现
1. Linux平台实时流媒体编程
1) PC机上的环境搭建
2) 嵌入式版的环境搭建
3) 初始化
4) 数据发送
5) 数据接收
2. Window平台实时流媒体编程
3. 具体流程图
三. 调试记录
四. 存在的问题和拟采取的解决方案
五. 作者简介
此模块是在基于S3C2440的嵌入式开发板上实现对摄像头采集并经过H.264压缩过的数据的RTP传输,这里边使用的H.264编码属于软编码,需要向PC机和嵌入式设备上移植相同版本的软编码库文件,并在接收端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]# cd jrtplib-3.7.1
[root@linuxgam jrtplib-3.7.1]# ./configure
[root@linuxgam jrtplib-3.7.1]# make
最后再执行如下命令就可以完成JRTPLIB的安装:
[root@linuxgam jrtplib-3.7.1]# make install
再进入jthread-1.2.1目录下重复以上操作:
[root@linuxgam src]# cd jthread-1.2.1
[root@linuxgam jrtplib-3.7.1]# ./configure
[root@linuxgam jrtplib-3.7.1]# make
[root@linuxgam jrtplib-3.7.1]# make install
生成的动态库安装到了/usr/local/lib目录下
头文件在/usr/local/include目录下
嵌入式版上的环境搭建和PC机上有些不同,如不注意可能导致两个库都不能使用。
首先,必须先安装jthread库,再安装jrtplib库;
其次,要交叉编译,需修改configure文件。
具体步骤为:
[root@linuxgam src]# cd jthread-1.2.1
[root@linuxgam jrtplib-3.7.1]# ./configure --host=arm-linux --prefix=/usr/local/arm/jrtplib/jrtpthread (说明:指定运行环境为嵌入式版本,和make install安装目录)
[root@linuxgam jrtplib-3.7.1]# make
[root@linuxgam jrtplib-3.7.1]# make install
[root@linuxgam src]# cd jrtplib-3.7.1
[root@linuxgam jrtplib-3.7.1]# ./configure -host=arm-linux -prefix=/usr/local/arm/jrtplib/jrtplib
运行完这步之后要先修改 src 目录下的rtpconfig_unix.h文件(具体方法在调试记录部分有阐述)
注释掉:RTP_BIG_ENDIAN , 具体说明看调试记录
然后再运行
[root@linuxgam jrtplib-3.7.1]# make
[root@linuxgam jrtplib-3.7.1]# make install
在使用JRTPLIB进行实时流媒体数据传输之前,首先应该生成RTPSession类的一个实例来表示此次RTP会话,然后调用Create()方法来对其进行初始化操作。RTPSession类的Create()方法只有一个参数,用来指明此次RTP会话所采用的端口号。
当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);
对于流媒体数据的接收端,首先需要调用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 generationdebug mulitithreaded 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这个文件,里面果然定义了一个RTP_BIG_ENDIAN,所以要和pc采用的小端字节序一样,先是直接在rtpconfig_unix.h中注释掉了
然后在重新编译库,执行
./configure –host=arm-linux –prefix=/usr/local/arm/jrtplib/jrtplib
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("write outfile 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。