DM8168移植wis-streamer【8168定制】

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中进行。

DM8168移植wis-streamer【8168定制】_第1张图片

(2)RTSP交互处理

DM8168移植wis-streamer【8168定制】_第2张图片

图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发送给客户端。

DM8168移植wis-streamer【8168定制】_第3张图片

图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

你可能感兴趣的:(【达芬奇技术】DM8168)