8168网传模块主要由wis-streamer进程进行,主要功能是实现高清视频和音频的网络传输,它能够实现以下几个方面的功能:
(1)能够对来自客户端的视频点播请求做出响应,实现高清视频和音频码流的实时传输功能。
(2)能够支持单播和组播两种网络传输方式,且单播支持TCP与UDP方式。
禁止私自转载(http://blog.csdn.net/guo8113/article/details/50241971)
1.1 启动方法
./wis-streamer-p 8557 -shm 33 -sem 1221 1220 &
./wis-streamer-s -p 8561 -shm 34 -sem 1225 1224 &
参数说明:
-s 为静音单播传输
-m 为组播传输
-p 指定端口号
-shm指定共享内存号
-sem指定两个信号量ID
注:在启动组播传输(组播未测试)前,必须要设定板卡的网关,方法为:
$routeadd default gw 192.168.1.1 eth0
接收视频流的地址为:
rtsp://192.168.1.8:8557/PSIA/Streaming/channels/H.264
rtsp://192.168.1.8:8561/PSIA/Streaming/channels/H.264
1.2 主要工作流程
(1)RTSP服务器初始化
首先按照图3.1开始进行RTSP服务器的初始化,这个过程在wis-streamer.cpp中进行。
(2)RTSP交互处理
图3.2
RTSP交互主要由RTSPClientSession对象管理和实现交互功能。任务调度器在监听到有客户端的连接请求时,会建立RTSPClientSession对象来负责处理与客户端的对话。在初始化RTSPClientSession对象时,RTSP服务器会将请求处理函数句柄incomingRequestHandler()以及对应的socket句柄一起传入任务调度器。incomingRequestHandler()的处理请求过程如上图所示。函数首先从socket读取请求报文,然后调用parseRTSPRequestString()函数来分析报文中各个字段的内容,提取出指令码。对于不同的指令码,进入不同的函数进行处理。
(3)RTP打包与发送
a、开启码流打包过程
startStream函数内部调用了streamstate->startPlaying(),具体函数实现在MediaSink::startPlaying()中,主要是检查source和sink是否已经初始化成功。
b、各媒体流子类的特殊处理
MediaSink::startPlaying()函数返回一个虚函数continuePlaying()进行进一步封装。continuePlaying()函数定义在MultiFramedRTPSink中,具体实现要在其子类中。所以,视频流将调用H264VideoRTPSink中的continuePlaying()做特殊处理,而音频码流不需要处理,然后回到MultiFramedRTPSink中调用buildAndSendPacket()。
c、设置RTP头
buildAndSendPacket()函数主要进行RTP头的设置,它主要由版本号、标志位、负载类型、序列号、时间戳和同步源SSRC等12字节组成。这边先设置了版本号、负载类型、序列号以及同步源信息,为时间戳和标志位留出了位置,因为要在打包的时候实时更新这两个数据。
d、开始数据打包
packFrame()函数负责数据的打包,首先检查是否还有上次打包时剩下的帧数据(因为一帧数据非常大,不可能一次就打包完),如果还有帧数据,则调用afterGettingFrame1();如果没有,就要问source要帧数据。由WisInput对象中的readFromfile()函数从共享内存获取。afterGettingFrame1()函数主要负责处理帧的分片封包。
e、RTP包的发送
sendPacketIfNecessary()函数功能为控制RTP的发送,通过uSecondsToGo变量控制下次打包的时间来调节音视频的发送速度,然后调用RTPInterface类中的sendPacket()函数完成数据的发送工作,主要就是给RTP包加上TCP或UDP头,然后送入socket发送给客户端。
图3.3
1.3 主要代码修改
(1)在live555官网下载wis-streamer的开源代码wis-streamer.tar.gz,在终端输入tar zxvf wis-streamer.tar.gz进行解压,将wis-streamer文件夹及其代码拷贝到live555的解压文件live下,与live555的4个库文件BasicUsageEnvironment、groupsock、liveMedia、以及UsageEnvironment放在一个目录下。
(2)在wis-streamer目录下模仿已有文件WISMPEG4VideoServerMediaSubsession.cpp和WISMPEG4VideoServerMediaSubsession.hh新增文件WISH264VideoServerMediaSubsession.cpp和WISH264VideoServerMediaSubsession.hh.
将WISMPEG4VideoServerMediaSubsession*WISMPEG4VideoServerMediaSubsession
::createNew(UsageEnvironment&env, WISInput& wisInput, unsigned estimatedBitrate) {
return new WISMPEG4VideoServerMediaSubsession(env,wisInput, estimatedBitrate);
}
改为:
WISH264VideoServerMediaSubsession*WISH264VideoServerMediaSubsession
::createNew(UsageEnvironment&env, WISInput& H264Input, unsigned estimatedBitrate) {
return newWISH264VideoServerMediaSubsession(env, H264Input, estimatedBitrate);
}
并且修改RTPSink*WISMPEG4VideoServerMediaSubsession
::createNewRTPSink(Groupsock*rtpGroupsock,
unsigned charrtpPayloadTypeIfDynamic,
FramedSource*/*inputSource*/) {
setVideoRTPSinkBufferSize();
returnMPEG4ESVideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic);
}
改为:
RTPSink*WISH264VideoServerMediaSubsession
::createNewRTPSink(Groupsock*rtpGroupsock,
unsigned charrtpPayloadTypeIfDynamic,
FramedSource*/*inputSource*/) {
setVideoRTPSinkBufferSize();
char BuffStr[200];
extern int GetSprop(void *pBuff);
GetSprop(BuffStr);
return H264VideoRTPSink::createNew(envir(),rtpGroupsock,96, 0x640032, BuffStr);
}
其中GetSprop函数在WISInput.cpp中定义,主要是对h.264的SPS和PPS信息进行base64编码。
(3)修改wis-streamer.cpp
a、定义了共享内存的标示符,创建了共享内存。
b、创建TaskScheduler* scheduler =BasicTaskScheduler::createNew();
UsageEnvironment* env =BasicUsageEnvironment::createNew(*scheduler);
TaskSecheduler类是一个任务调度器,是整个Live555的任务调度中心,主要负责任务的调度和执行。UsageEnvironment代表了整个系统的运行环境,主要用于错误的输入和输出。
c、StreamingMode streamingMode =STREAMING_UNICAST;设置采用的码流传送方式为单播。
d、设定启动时后面可带的控制参数,“-m”为组播,“-s”为静音(不要音频),“-a”为选择的音频编码方式为aac。启动格式如下:./wis-streamer -s 表示静音启动wis-streamer。
e、用fork函数是创建子进程。
对于不同的编码方式可以进入不同的子进程,这样就可以同时开几种编码方式的服务器,目前注释掉的原因是现在只加入了H.264的网传,所以暂时只开了一个进程,后续要添加别的编码方式时可以将child[ ]的个数增加来增添子进程个数。
f、为每种编码格式的视频设定一些参数,包括video_type,rtspserverport,视频流bitrate以及音视频rtpport。
g、初始化每种编码格式的输入接口。WISInput类在wisinput.cpp中
H264InputDevice=WISInput::createNew(*env,video_type);
if(H264InputDevice == NULL) {
err(*env)<< "Failed to create H264 input device\n";
exit(1);
h、建立rtsp server, 用来创建socket,并绑定和监听端口。
RTSPServer* rtspServer =NULL;
rtspServer =RTSPServer::createNew(*env, rtspServerPortNum, NULL);
i、创建描述该媒体流的会话,包括流的名称和sdp等。
添加视频流的子回话,此处添加的为H.264视频流,包括输入接口,视频比特率。
如果音频不是在静音情况下,则继续添加音频流子回话,此处添加的是G711,也就是PCM的u律。
j、char *url =rtspServer->rtspURL(sms);
获得服务器的IP地址,此处是自动获得板卡IP,无需手动设置。
k、env->taskScheduler().doEventLoop();
SingleStep()函数主要完成两种任务,分别是socket任务(socket handler)和延迟任务(delay task)。Socket任务主要是用于执行socket操作的任务,对已经准备好的socket进行读写操作。延迟任务主要是一个延迟队列,队列中每一项代表了一个要调度的任务,同时保存了距离执行这个任务剩下的时间,来进行任务的先后调度。
如果是组播模式,chooseRandomIPv4SSMAddress()函数将从[232.0.1.0, 232.255.255.255)随机选择一个地址作为组播地址。
rtpGroupsockVideo = newGroupsock(*env, dest, rtpPortVideo, ttl);
rtcpGroupsockVideo= new Groupsock(*env, dest, rtcpPortVideo, ttl)
分别为音视频设置RTP、RTCP组播端口号,再设置TTL(生存时间)指定数据包被路由器丢弃之前允许通过的网段数量,接着用组播地址和服务器地址构造一个组播组。
创建videosource、audiosource获取音视频源码流,相应的创建videosink和audiosink接收source送来的码流。
sinkVideo->startPlaying(*sourceVideo,NULL, NULL);
sinkAudio->startPlaying(*sourceAudio,NULL, NULL);
开始向支持组播的路由器发送RTP数据。组播与单播不同的是,它并不需要客户端请求后才开启码流打包传输工作,而是在RTSP服务器初始化阶段已经开始传输码流。
(4)修改WISInput.cpp
a、加入头文件share_mem.h、semaphore.h,因为share_mem.c和semaphore.c为c语言文件,wis-streamer为c++文件,引用时方法如下:
extern "C"
{
#include"share_mem.h"
#include"semaphore.h"
}
b、同样和8168前端一样,加入音视频信号量
int semEmpty; int semFull; int semEmpty_audio; intsemFull_audio;
c、在class WISOpenFileSource类中加入以下变量:
public:
int uSecsToDelay; //任务调度器等待的时间
int uSecsToDelayMax; //同上
int srcType; //数据源类型,srcType=0为视频,srcType=1为音频
d、在视频source类中加入以下变量:
int nal_state; //nal帧类型
int startI; //I帧是否开始
e、在音频source类中加入以下变量和函数:
int getAudioData(); //获取音频帧数据的函数
struct timeval fPresentationTimePre;
int IsStart; //是否开始传输音频数据
f、改写creatNew函数
WISInput*WISInput::createNew(UsageEnvironment& env ,int vType) {
return new WISInput(env,vType);
}
g、改写wisinput函数,初始化视频和音频source
WISInput::WISInput(UsageEnvironment&env,int vType)
:Medium(env),videoType(vType),fOurVideoSource(NULL),fOurAudioSource(NULL){
}
改写析构函数~WISInput函数,删除音视频source
h、修改voidWISOpenFileSource::doGetNextFrame()函数
改为只保留incomingDataHandler(this);函数
i、改写void WISOpenFileSource::incomingDataHandler1()函数
加入对readfromFile函数的返回值的不同处理:
对于ret<0,handleClosure(this);
对于ret=0,延迟一段时间后,继续执行incomingDataHandler
对于ret>0,执行aftergetting;
j、改写WISVideoOpenFileSource和WISAudioOpenFileSource类的构造函数:
对各成员变量进行初始化,并对delay的时间进行设置。
k、改写视频readfromfile函数:intWISVideoOpenFileSource::readFromFile(),这里的代码量很大,就不贴出来了,主要做的工作为从共享内存获取数据。
在该函数中用到了AV_DATA这个结构体变量,里面存放了帧序列号(暂时没用到,先预留着),帧大小,是否是I帧以及时间戳信息。这个结构体里存放的数据是要从共享内存接数据用的。
ShareMemRead(&av_data,buffer); 进入共享内存读取数据。在ShareMemRead()函数中,shared_use *shared_stuff这个结构体指针绑定共享内存,这个结构体中包含了视频结构体VideoBuf,音频结构体AudioBuf,在8168前端和wis这端定义的必须是一样的。然后把码流数据和一些参数从共享内存拷贝到buffer和AV_DATA中。
然后是一个大的if从句:if(av_data.isIframe)判断如果是I帧的话进入这个从句。这个if从句的目的就是把sps、pps和I帧分开打包。这边特别要注意的是,在读到sps和pps时并没有释放共享内存,因为如果读完就释放的话,8168前端就会放新的数据进去,所以这边直到I帧结束才释放了共享内存。
采取的方法是:原本一对锁应该是semaphore_p(semFull)和semaphore_v(semEmpty),而如果是希望不释放的话,应该是semaphore_p(semFull)和semaphore_v(semFull)。
把sps和pps以及I帧存放到fTo后,调用gettimeofday(&fPresentationTime,NULL);来获取系统时间作为时间戳(注意,我们此处并没有使用8168前端传来的是时间戳,而是获取的当前时间)。
if(!startI)这个从句的目的就是如果第一帧不是I帧,那么直接舍弃,返回0,delay一段时间继续进入incomingDataHandler直到找到I帧为止。
接下来的操作就说针对P或B帧的,直接将其拷贝到fTo指针指向的地址,fFrameSize就是一帧的大小。然后获取当前时间作为系统时间。然后返回1,走到afterGetting。
l、改写音频readfromfile函数:intWISAudioOpenFileSource::readFromFile()
if( IsStart||I_start)
{
ret = getAudioData();
IsStart = 0;
}
这边IsStart是因为在启动流的时候,必须播放一下音频,不然就无法启动,vlc会报错,所以这边初始化时将IsStart设为1,播放一次后变为0。I_start是为了让音频在视频帧出来后再播放音频(也就是视频找到I帧开始封装后才让音频也封装)。
getAudioData();就是进入共享内存拷贝出数据的部分,在进入共享内存前先将信号量初始化,然后调用AudioShareMemRead(&av_data,buffer);获取音频数据,将其放到fTo。
m、编写GetSprop函数
其中调用ret =GetVolInfo(tempBuff,sizeof(tempBuff)); 这个函数的作用就是从共享内存处读取一帧数据(因为这边是首次连接,第一帧一定是I帧,并且前面含有sps和pps信息)此函数仅仅是把第一帧数据读取,然后存到pbuff中。
然后回到GetSprop中,从刚才获取的buffer中解析出sps和pps(sps是00000001*7开始的,pps是00000001*8开始),解析出sps和pps后,将其进行编码,生成的就是放到config(SDP中sprop-parameter-sets)里的sps和pps信息了。
(5)改写live555的livemedia库文件
a、将DM365源代码中livemedia文件下的H264VideoRTPSink.cpp、H264VideoRTPSource.cpp、H264VideoStreamFramer.cpp以及H264VideoStreamParser.cpp文件替换掉官网最新下载的live555的livemedia库文件下的上述文件,同时将livemedia下的include下的上述cpp文件的.hh文件也替换掉。
b、修改livemedia下的MultiFramedRTPSink.cpp文件
在voidMultiFramedRTPSink::sendPacketIfNecessary()函数中,加入如下内容:
if(fRTPPayloadType == 0)
{
uSecondsToGo = 64000;
}
因为经测试发现,视频帧因为分辨率高,数据量很大,需要打包的个数也很多,耗时明显比音频打包的时间长,然后用VlC播放的时候,视频放不出来,音频很流畅。分析原因,发现音频包打包太快,音频包数据就优先排队在socket端口,导致了端口的独占,视频包虽然打包好了,但是无法从端口处发送出去。所以我的解决办法是让音频下次打包的时间间隔延长,fRTPPayloadType=0时表示的是音频(H.264是96),这边延长时间为多少还不确定,暂时设为一个音频包播放时间的一半(一个音频包播放的时间为128000ms),也可以使用别的方法解决,有待研究。
1.4 代码移植和编译
(1)live555移植
Live555开源代码可以到官网上下载,它可以移植到很多不同的平台,包括linux、armlinux、windows、os等,移植到DM8168板卡上的步骤如下:
a、解压软件包。在终端输入tar zxvf live555-latest.tar.gz,出现live文件夹。
b、修改配置文件。进入live文件夹,找到并打开config.armlinux文件,将
CROSS_COMPILE? = arm-elf- 修改为DM8168板卡的交叉编译工具
CROSS_COMPILE? = arm-none-linux-gnueabi-
c、剪裁Live555。在live文件下有很多文件,而在建立本服务器时,只有4个库文件是必须的,分别是BasicUsageEnvironment、groupsock、liveMedia、以及UsageEnvironment,所以只保留这几个文件夹,并且修改makefile文件,将与编译mediaServer和testProgs的相关内容删去。
d、编译库文件。在终端中输入./genMakefile armlinux,然后make。在c中所述的4个文件夹中将生成4个静态库文件,分别是libBasicUsageEnvironment.a、libgroupsock.a、libliveMedia.a 以及libUsageEnvironment.a。
(2)wis-streamer的makefile文件的修改
a、设置编译工具
CC =arm-none-linux-gnueabi-gcc
CPLUSPLUS =arm-none-linux-gnueabi-g++
#CC=gcc
#CPLUSPLUS=g++
b、设置头文件路径,主要为加入live555的4个库文件的include
INCLUDES = -I . \
-I$(LIVE_DIR)/BasicUsageEnvironment/include \
-I$(LIVE_DIR)/UsageEnvironment/include \
-I$(LIVE_DIR)/groupsock/include \
-I$(LIVE_DIR)/liveMedia/include
c、设置静态链接库文件路径,主要加入live555的4个静态链接库。
LIBS = -L$(LIVE_DIR)/liveMedia -lliveMedia \
-L$(LIVE_DIR)/BasicUsageEnvironment -lBasicUsageEnvironment \
-L$(LIVE_DIR)/UsageEnvironment -lUsageEnvironment \
-L$(LIVE_DIR)/groupsock –lgroupsock
d、设置生成的OBJ文件
OBJS = wis-streamer.o Err.o WISInput.o \
share_mem.o \
semaphore.o \
WISServerMediaSubsession.o \
WISH264VideoServerMediaSubsession.o \
WISPCMAudioServerMediaSubsession.o \
e、设置c++文件依赖的头文件
wis-streamer.cpp: Err.hh
Err.cpp: Err.hh
WISInput.cpp: WISInput.hh share_mem.hsemaphore.h
share_mem.c: share_mem.h
semaphore.c: semaphore.h
WISServerMediaSubsession.cpp: WISServerMediaSubsession.hh
WISServerMediaSubsession.hh: WISInput.hh
WISH264VideoServerMediaSubsession.hh: WISServerMediaSubsession.hh
WISH264VideoServerMediaSubsession.cpp: WISH264VideoServerMediaSubsession.hh
WISPCMAudioServerMediaSubsession.cpp: WISPCMAudioServerMediaSubsession.hh
(3)编译wis-streamer
将(2)中的makefile文件进行编译,生成二进制的可执行文件wis-streamer,将其拷贝到8168板的NFS目录下。
1.5 进一步修改与定制与当前版本说明
主要对main函数中的参数解析部分进行了调整,可以指定共享内存和信号量的ID,对程序中相关的代码进行了修改,实现通过指定参数可以运行多次wis-streamer实现多路的RTP。
1.将gSEMV和gSEMVE在./wis-streamer/WISInput.cpp中声明为全局变量,并在wis-streamer.cpp中声明外部变量。
externint gSEMV;
externint gSEMVE;
2.在main中对参数解析:
3.另外对代码进行了部分的重构主要集中在WISVideoOpenFileSource类。如:WISVideoOpenFileSource::readFromFile()
QQ群交流139696200