在Livemedia的基础上开发自己的流媒体客户端

Livemedia的基础上开发自己的流媒体客户端 V 0.01

桂堂东

一、背景... 3

二、Livemedia框架介绍... 4

1.总体框架... 4

2.客户端框架... 4

2.1 客户端openRTSP流程... 4

2.2增加一种新的媒体... 9

2.2.1增加媒体的format 9

2.2.2 新媒体需要考虑的问题... 10

2.3类详细说明... 13

2.3.1 BasicUsageEnvironment, UsageEnvironment 13

2.3.2 groupsock. 15

2.3.3 livemedia. 15

三、一些总结... 16

A. Buffer 管理... 16

B. How to control the receive loop. 19

C. PAUSE&SEEK. 19

D. 释放资源问题... 20

 

一、背景

       如今流媒体无处不在,而主流流媒体服务器为RealworksWindows Media ServerApple Darwin server, 而客户端程序,即包括会话建立、接收以及解码播放,则百花齐放,如何利用一种开源的代码实现自己的流媒体客户端,同时可以支持新的媒体格式呢?这是本文重点所在。

       公司接触一个项目,要求能够按照3GPP的标准,实现RTSP/RTP协议以及对RTP包进行解析(独特格式),解码以及播放,因为时间比较紧,因此考虑在一种比较稳定并全面的开放源码标准基础上进行二次开发,主要是对新媒体的支持、bug-fix以及架构的调整。

 

 

 

二、Livemedia框架介绍

详细的帮助文档见www.live.com/livemedia

1.总体框架

Live的网站上有doxgen产生的帮助文档以及各个类之间的相互关系,这里不再螯述,不过这里要提醒的是,live的库代码可以同时供服务器和客户端使用,因此如果只是开发单个程序或者需要把服务器和客户端的程序分割清楚的话,最好先将代码剥离,这里可以参考live的参考例子openRTSP以及TestOnDemandServer

2.客户端框架

 

 

2.1 客户端openRTSP流程

这里给出了openRTSP的流程,同时最后给出了接收packet循环中的操作顺序,最后将会叙述奖励客户端需要建立些什么。

Ø         “ || “ present that this item is depend on the input execute parameter

 

1.    Socket environment initial. 

 

2.    Parse the input parameters.

 

3.    CreateClient. Get class RTSPClient construct (return class medium and some vars)

3.1 For the class Medium

3.1.1 // First generate a name for the new medium: and put into the result buffer

3.1.2 // Then add it to our table: (It’s a hash table store the medium session, should has a MAX store value, in other words, the client should handle limited medium session)

  3.2 RTSPClient variables initial and construct RTSP “User-Agent”

 

4.    Send RTSP “options“ and get OPTIONS from server.

 4.1 Create Socket connection

 4.2 Send OPTIONS string

 4.3 Get response from the server (If response code is 200 and it’s supported public method|OPTIONS)

 

5.    Get SDP description by the URL of the server(return value:SDPstring)

 5.1 Create Socket connection

 5.2 Check if the URL has username and password 

 5.3 Send OPTIONS string

  5.3.1 construct Authentication

  5.3.2 construct DESCRIPS string and send

 5.4 Get response from the server 

5.4.1 If response code is what we can handle?

5.4.2 find the SDP descriptor and do some validate check

 

6.    Create media session from the SDP descriptor above.

6.1 session=mediasession::createNew 

 6.1.1 for the class Medium(this is different from the medium of class RTSPClient)

(1.1) // First generate a name for the new medium: and put into the result buffer

(1.2) // Then add it to our table:       (It’s a hash table store the medium session, should has a MAX store value, in other words, the client should handle limited medium session)

    (2) Some variables initial, such as subsession (m= present a new subsession) and CNAME etc

   6.1.2 initial the mediasession with the SDP info

(1) Parse SDP string, get the key and related value to the var.

(2) Get the “m=”(If there have) and create subsession

Decide use UDP or RTP; Mediumname; protocol; payload format etc.

6.2 initial of the MediaSubsessionIterator (Using the session and subsession(m))

 6.1.1 Check the subsession’s property and set some Var.

 6.2.2 for the receivers [receive data but not 'play' the stream(s)]

  (1) subsession->initial()

    (1.1) Create RTP and RTCP 'Groupsocks' on which to receive incoming data.

    (1.2) According the protocol name, create out UDP or ‘RTP’ special source 

    (1.3)Create RTCPInstance  []

      (1.3.1) // Arrange to handle incoming reports from others:

        (1.3.2)// fRTCPInterface.startNetworkReading(handler);

        (1.3.3)// Send our first report. Which compose with RR and SDES(CNAME) to the server

 (2set the big threshold time, for reorder the incoming packet and restore it. Maybe set the receiveBufferSize (if we set it in the input parameter)

   6.2.3 for the player (not recoding the stream, instead, 'play' the stream(s))

         Just do nothing here, waiting the follow action.

6.3 SetupStreams(RTSP “SETUP”)

Perform additional 'setup' on each subsession, before playing them:

For each subsession, RTSPClient->setupMediaSubsession(*)

6.3.1 // First, construct an authenticator string:

6.3.2 // When sending more than one "SETUP" request, include a "Session:" header in the 2nd and later "SETUP"s.

6.3.3 // Construct a standard "Transport:" header. [see the appendix (1)]

6.3.4 Send request string and get response, 

(1) Check the validation(such as response code

(2)// Look for a "Session:" header (to set our session id), and a "Transport: " header (to set the server address/port)

(3) If the subsession receive RTP (and send/receive RTCP) over the RTSP stream, then get the socket connect changed to the right way

 

7.    Create output files: Only for the Receiver (Store the streaming but not play it)

For different file format, use different *FileSink class

This uses the QuickTime file as demo. Output to the ‘::stdout’

7.1 qtout = QuickTimeFileSink::createNew(***)

 7.1.1 For construct class medium again, see the front for detail.

 7.1.2 Some variables get their initial value

 7.1.3 // Set up I/O state for each input subsession:

   (1)  // Ignore subsessions without a data source:

   (2) // If "subsession's" SDP description specified screen dimension or frame rate parameters, then use these.  (Note that this must be done before the call to "setQTState()" below.)

   (3) Maybe create a hint track if input parameter contains it

   (4) // Also set a 'BYE' handler for this subsession's RTCP instance:

(5)  // Use the current time as the file's creation and modification time.  Use Apple's time format: seconds since January 1, 1904

 7.1.4 startPlaying (details in 7.2)

|| 7.2 Common File

 7.2.1 filesink = FileSink::createNew(***)

  (1) first use MediaSink (use class Medium constructor again, see the front)

 (2) some variables got initial values.

 7.2.2 filesink->startPlaying(actually using the parent function mediasink->st.)

  (1) Check, such as // Make sure we're not already being played; our source is compatible:

  (2) ContinuePlaying()

   (2.1) FramedSource::getNextFrame  (source type was appointed in the startplaying…as FrameSource)

check and valued some callback function: // Make sure we're not already being read:

“Different media source”->doGetNextFrame() //such as Mp3FromADUSource virtual func.

In this function // Before returning a frame, we must enqueue at least one ADU:

        OR // Return a frame now:

 

8 startPlayingStreams 

// Finally, start playing each subsession, to start the data flow:

 8.1 rtspClient->playMediaSession(*)

 8.1.1 check validation 

// First, make sure that we have a RTSP session in progress

 8.1.2 Send the PLAY command:

  (1) // First, construct an authenticator string:

  (2) // And then a "Range:" string:

  (3) Construct “PLAY” string 

  (4) Send to server

  (5) Get response. And check response code / Cseq /…

 8.2 // Figure out how long to delay (if at all) before shutting down, or repeating the playing

 || 8.3 checkForPacketArrival   //see if there any packet coming in the subsessions.

 || 8.4 checkInterPacketGaps   // Check each subsession, counting up how many packets have been received:

 

9 env->taskScheduler().doEventLoop()

Main loop for get the data from the server and parse and store or play directly.

9.1 BasicTaskScheduler0::doEventLoop, 

will loop use SingleStep

9.2 BasicTaskScheduler::SingleStep

    See if there any readable socket in the fReadSet(store the socket descriptor of the subsession) and if have will handle it

(1)  fDelayQueue.handleAlarm();

(2)  (*handler->handlerProc)(handler->clientData, SOCKET_READABLE); loop handle the subsession task.  

[this is MultiFramedRTPSource:: networkReadHandler]

(3)  MultiFramedRTPSource:: networkReadHandler

// Get a free BufferedPacket descriptor to hold the new network packet:

          BufferedPacket* bPacket

= source->fReorderingBuffer->getFreePacket(source);

           // Read the network packet, and perform sanity checks on the RTP header:

              if (!bPacket->fillInData(source->fRTPInterface)) //The coming packet not belongs cur session

         // Handle the RTP header part

         // The rest of the packet is the usable data.  Record and save it(To the recordingBuffer)

Boolean usableInJitterCalculation  //RTCP jitter calculate

             = source->packetIsUsableInJitterCalculation((bPacket->data()),bPacket->dataSize());

         source->receptionStatsDB()   // Note that we have reve a rtp packet

                    .noteIncomingPacket(rtpSSRC, rtpSeqNo, rtpTimestamp,

                                                                  source->timestampFrequency(),

                                                                  usableInJitterCalculation, presentationTime,

                                                                  hasBeenSyncedUsingRTCP, bPacket->dataSize());

         // Fill in the rest of the packet descriptor, and store it:

bPacket->assignMiscParams(rtpSeqNo, rtpTimestamp, presentationTime,

                                                                   hasBeenSyncedUsingRTCP, rtpMarkerBit,

                                                                   timeNow);

         //Store the packet.

    source->fReorderingBuffer->storePacket(bPacket);

Then

         source->doGetNextFrame1();// If we didn't get proper data this time, we'll get another chance

9.3 MultiFramedRTPSource::doGetNextFrame1()

To MultiFramedRTPSource or some other inherit class

(1)// If we already have packet data available, then deliver it now.

         BufferedPacket* nextPacket

                            = fReorderingBuffer->getNextCompletedPacket(packetLossPrecededThis);

  (2// Before using the packet, check whether it has a special header

                            // that needs to be processed:

         if (!processSpecialHeader(nextPacket, specialHeaderSize))

         This is what the particular inherit class will do, for different packet format…

3Handle the packet data, for different RTP packet, it has different construct, so ***

   (4) // The packet is usable. Deliver all or part of it to our caller:

         nextPacket->use(fTo, fMaxSize, frameSize, fNumTruncatedBytes,

                                                        fCurPacketRTPSeqNum, fCurPacketRTPTimestamp,

                                                        fPresentationTime, fCurPacketHasBeenSynchronizedUsingRTCP,

                                                    fCurPacketMarkerBit);

---------unsigned frameSize = nextEnclosedFrameSize(newFramePtr, fTail - fHead);

         (5)    If we have all the data that the client wants then :

// Call our own 'after getting' function.  Because we're preceded

                            // by a network read, we can call this directly, without risking

                            // infinite recursion.

                            afterGetting(this);   

           ------------ void FramedSource::afterGetting(FramedSource* source)

                   --------- void FileSink::afterGettingFrame(

                               void FileSink::afterGettingFrame1

a.        addData(fBuffer, frameSize, presentationTime)

b.        continuePlaying();// Then try getting the next frame:

==

9.4 Boolean FileSink::continuePlaying()

         fSource->getNextFrame---------FramedSource->getNextFrame-------MultiFramedRTPSource->

9.5 void MultiFramedRTPSource::doGetNextFrame()

  (1) TaskScheduler::BackgroundHandlerProc* handler

              = (TaskScheduler::BackgroundHandlerProc*)&networkReadHandler;

    fRTPInterface.startNetworkReading(handler);

        doGetNextFrame1();  [Back to the section of 9.3] 

 

Note:

(1) For RealNetworks streams, use a special "Transport:" header, and also add a 'challenge response'.

(2) The detailed relationship of them doesn’t list because it is some complex and we should need more time.

(3) When we arrive the endTime that got from the SDP line or the server translate teardown info, then the client will stop

In the start function “startPlayingStream” it add the “ssessionTimerHandler” into the schedule.

 

从上面的流水帐我们可以看出利用live的代码创建一个传统的流媒体客户端的接收部分我们需要建立以下流程。

 

 

2.2增加一种新的媒体

一般基于多幀得数字媒体可以通过继承MultiFramedRTPSource实现自己得媒体类,同时需要继承PacketBuffer实现自己得包buffer管理,这里可以根据新媒体得RTP payload format 得格式进行操作,我们实现得新媒体类型,在下面会有详细描述。

2.2.1增加媒体的format

增加新媒体也是基于Frame格式的,这里每一幀称呼为MAUMedia Access Unit),而MAURTP packet中的组织不径相同。

As shown in Figure , the RTP Payload Format header is divided into three sections. Each section starts with a one-byte bit field, and is followed by one or more optional fields. In some cases, up to two entire sections may be omitted from the RTP Payload Format header. This can result in an RTP Payload Format header as small as one byte.

All RTP Payload Format fields should be transmitted in network byte order, which means that the most significant byte of each field is transmitted first.

The RTP Payload Format header is followed by a payload. The payload can consist of a complete MAU or a MAU fragment. The payload can contain a partial MAU, allowing large MAUs to be fragmented across multiple payloads in multiple RTP packets. 

The first payload can be followed by additional pairs of RTP Payload Format headers and payloads, as permitted by the size of the RTP packet. 

 

每一个包中MAU的组合形式有以下几种:

2.2.2 新媒体需要考虑的问题

A. 从上可以看出,新的媒体的每个RTP packet当中,可能含有一个或多个MAU亦或者MAUfragment,而在parse每个RTP packet之后需要将每个完整MAU的信息(数据,大小,以及PT: Presentation Time, DTS等)传给Decoder,但是Live得代码支持得多媒体格式中基本集中为单幀一个包或者说一包多幀然而所有得附加信息都集中在packet的首部,即标准RTP头的后面 :-)。因此在收取RTP packet后首先handle标准的RTP header之后(MultiFramedRTPSource::networkReadHandler×××)),将包丢入reorderdingBuffer,下一次取包处理特殊头的时候需要特殊处理,将单个包中所有的MAU或者MAU fragment的头信息以及大小等取出,在MultiFramedRTPSource::doGetNextFrame1()中综合处理

void MultiFramedRTPSource::doGetNextFrame1() 

{

         while (fNeedDelivery)   //Sure, see the front

         {

                   // If we already have packet data available, then deliver it now.

                   Boolean packetLossPrecededThis;

                   BufferedPacket* nextPacket  //Get the header packet, maybe the one which we just handled

                            = fReorderingBuffer->getNextCompletedPacket(packetLossPrecededThis);

                   if(nextPacket == NULL) 

                            break;

                   

                   fNeedDelivery = False;

                   

                   if (nextPacket->useCount() == 0) 

                   {

                            // Before using the packet, check whether it has a special header

                            // that needs to be processed:

                            unsigned specialHeaderSize;

                            if (!processSpecialHeader(nextPacket, specialHeaderSize)) {

                                     // Something's wrong with the header; reject the packet:

                                     fReorderingBuffer->releaseUsedPacket(nextPacket);

                                     fNeedDelivery = True;

                                     break;

                            }

                            nextPacket->skip(specialHeaderSize);

                   }

                   

                   // Check whether we're part of a multi-packet frame, and whether

                   // there was packet loss that would render this packet unusable:

                   if (fCurrentPacketBeginsFrame)  //In the processSpecialHeader(), it will change...

                   {       

                            unsigned PT_tem =0;   //Alexis

                            FramePresentationTime(PT_tem);

                            nextPacket->setPresentTime(PT_tem);                //Alexis 04-11-10

                            if (packetLossPrecededThis || fPacketLossInFragmentedFrame) //Packet loss and the former frame has unhandled fragment.

                            {

                                     // We didn't get all of the previous frame.

                                     // Forget any data that we used from it:

                                     fTo = fSavedTo; 

                                     fMaxSize = fSavedMaxSize;

                                     fFrameSize = 0;

                            }

                            fPacketLossInFragmentedFrame = False;  //begin frame, so ...                              

                   } 

                   else if (packetLossPrecededThis) 

                   {

                            // We're in a multi-packet frame, with preceding packet loss

                            fPacketLossInFragmentedFrame = True;

                   }

                   

                   if (fPacketLossInFragmentedFrame) 

                   {

                            //---Alexis 10-28

                            unsigned MauFragLength;

                            doLossFrontPacket(MauFragLength);

                            // get the length from now MAU fragment to the next MAU start

 

                            if(MauFragLength != 0)

                            {

                                     nextPacket->skip(MauFragLength);

                                     fNeedDelivery = True;

                                     break;

                            }

                            else  //The original part...

                            {

                                     //Normal case:This packet is unusable; reject it:

                                     fReorderingBuffer->releaseUsedPacket(nextPacket);

                                     fNeedDelivery = True;

                                     break;        

                            }                          

                   }

                   

                   // The packet is usable. Deliver all or part of it to our caller:

                   unsigned frameSize;

                   nextPacket->use(fTo, fMaxSize, frameSize, fNumTruncatedBytes,

                                                        fCurPacketRTPSeqNum, fCurPacketRTPTimestamp,

                                                        fPresentationTime, fCurPacketHasBeenSynchronizedUsingRTCP,

                                                        fCurPacketMarkerBit);

                   fFrameSize += frameSize;

                   

                   if (!nextPacket->hasUsableData()) {

                            // We're completely done with this packet now

                            fReorderingBuffer->releaseUsedPacket(nextPacket);

                   }

                   

                   if (fCurrentPacketCompletesFrame || fNumTruncatedBytes > 0) 

                   {

                            // We have all the data that the client wants.

                            if (fNumTruncatedBytes > 0) {

                                     envir() << "MultiFramedRTPSource::doGetNextFrame1(): The total received frame size exceeds the client's buffer size ("

                                               << fSavedMaxSize << ").  "

                                               << fNumTruncatedBytes << " bytes of trailing data will be dropped!/n";

                            }

                            // Call our own 'after getting' function.  Because we're preceded

                            // by a network read, we can call this directly, without risking

                            // infinite recursion.

 

                            afterGetting(this);  

                            //It will store the whole Frame to the outbuffer (in here is the file)

                   } 

                   else 

                   {

                            // This packet contained fragmented data, and does not complete

                            // the data that the client wants.  Keep getting data:

                            fTo += frameSize; 

                            fMaxSize -= frameSize;

                            fNeedDelivery = True;

                   }

         }

}

 

B.另外,由于每个MAU的开始都会有各自的时间信息,因此,Live的代码中以标准RTP包头中的Timestamp作为时间基准已经不再适应,需要我们自己设置时间,在传输单个MAU的时候, 上面代码中nextPacket->setPresentTime(PT_tem); //Alexis 04-11-10就是这个意思。

 

C.  

 

2.3类详细说明

livem的库分为BasicUsageEnvironment, UsageEnvironment, groupsock以及livemedia4个部分,其中BasicUsageEnvironment, UsageEnvironment负责任务的调度已经环境的配置,而groupsock则负责socks套接字的创建以及相应信息(询问信息以及数据信息)的发送接收。Livemedia则是整个工程的核心,负责rtsp(client,server)session(subsession)rtcpinstance***Source***Sink的运转,下面将会一一详细介绍。

2.3.1 BasicUsageEnvironment, UsageEnvironment

UsageEnvironment

HashTable http://www.live.com/liveMedia/doxygen/html/classHashTable.html

哈希链表的建立与维护

类中定义了class Iterator // Used to iterate through the members of the table: 

 

TaskScheduler

// Schedules a task to occur (after a delay) when we next reach a scheduling point.

ScheduleDelayedTask(*)   unscheduleDelayedTask(*)

// For handling socket reads in the background:

BackgroundHandlerProc(*)  

turnOnBackgroundReadHandling(int sockNum)  turnOffBackgroundReadHandling(int sockNum)

 

doEventLoop(char* watchVariable = NULL) = 0;

// Stops the current thread of control from proceeding, but allows delayed tasks (and/or background I/O handling) to proceed.

 

StrDup

字符串拷贝, 

 

UsageEnvironment (http://www.live.com/liveMedia/doxygen/html/classUsageEnvironment.html)

// An abstract base class, sub-classed for each use of the library

 

BasicUsageEnvironment

BasicHashTable http://www.live.com/liveMedia/doxygen/html/classBasicHashTable.html) 

// A simple hash table implementation, inspired by the hash table

// implementation used in Tcl 7.6: <http://www.tcl.tk/>

class BasicHashTable: public HashTable

内部class Iterator: public HashTable::Iterator

 

BasicUsageEnvironment0

// An abstract base class, useful for sub-classing (e.g., to redefine the implementation of "operator<<")

class BasicUsageEnvironment0: public UsageEnvironment

定义了变量 unsigned fCurBufferSize;  unsigned fBufferMaxSize;

// An abstract base class, useful for sub-classing (e.g., to redefine the implementation of socket event handling)

class BasicTaskScheduler0: public TaskScheduler

 

BasicUsageEnvironment

class BasicUsageEnvironment: public BasicUsageEnvironment0

构造static BasicUsageEnvironment* createNew(TaskScheduler& taskScheduler);

 

class BasicTaskScheduler: public BasicTaskScheduler0

定义了  int fMaxNumSockets;  fd_set fReadSet;   / / To implement background reads:

 

2.3.2 groupsock

// "mTunnel" multicast access service

NetCommon

对于不同平台,包含不同的sock头文件   Win32 WinCE VxWorks UNIX SOLARIS

――― #include <winsock2.h>

――― #include <ws2tcpip.h>

NetAddress

// Definition of a type representing a low-level network address.

// At present, this is 32-bits, for IPv4.  Later, generalize it,

// to allow for IPv6.

class NetAddress

class NetAddressList

class Port

class AddressPortLookupTable// A generic table for looking up objects by (address1, address2, port)

MediaSubsessioninitiate()时候使用NetAddressList来初始话服务器返回的SDP信息中”C后面的IP地址信息

NetInterface  (http://www.live.com/liveMedia/doxygen/html/classNetInterface.html)

class NetInterface

class DirectedNetInterface: public NetInterface         负责写

class DirectedNetInterfaceSet                                         负责DirectedNetInterface集的管理

class Socket: public NetInterface                                 负责读

class SocketLookupTable                                                负责查找Sock通过port

class NetInterfaceTrafficStats                                         负责计算Traffic(多少个包和字节数)

Inet.c

定义了initializeWinsockIfNecessary 以及 随机数产生函数,被GroupsockHelper调用

GroupsockHelper

定义 创建数据报UDP或者流TCP sock连接(并bind()端口)读写socket,设置socket参数的头文件,以及关于SSMspecial source multicast)的说明

TunnelEncaps       // Encapsulation trailer for tunnels

class TunnelEncapsulationTrailer

IoHandlers           // not used in the current version?

// Handles incoming data on sockets:

GroupEId      endpoint ID

// used by groupsock

Groupsock   (http://www.live.com/liveMedia/doxygen/html/classGroupsock.html)

// An "OutputSocket" is (by default) used only to send packets.

// No packets are received on it (unless a subclass arranges this)

class OutputSocket: public Socket

// An "OutputSocket" is (by default) used only to send packets.

// No packets are received on it (unless a subclass arranges this)

class destRecord

class Groupsock: public OutputSocket

// A "Groupsock" is used to both send and receive packets.

// As the name suggests, it was originally designed to send/receive

// multicast, but it can send/receive unicast as well.

其中包含增加,移除address, 同时对于判断(组/单播),TTL

class GroupsockLookupTable

// A data structure for looking up a 'groupsock'

// by (multicast address, port), or by socket number

 

2.3.3 livemedia

这个部分是live.com得代码核心,要实现RTSP得建立,控制,以及RTP传输得建立

以及各种RTP payload得打包以及解析。这里我将分为两部分介绍,第一部分介绍创建session得基本环境,第二部分着重说明RTP payload format

2.3.3.1 最小环境得建立

1. our_md5/our_md5hl

如果你不需要对URL中进行authentication得话,这个部分可以忽略

2. 层次介绍

 

AMedium

 

//Basic class of the LiveMedia,定义了一些纯虚函数

class Medium

后面所有的类都是继承Medium或者其派生类的

对于每个媒体(subsession?),都会创建一个mediumname,同时多个medium将分享一个UsageEnvironment类的变量(内部定义了字符输出操作符以及任务调度函数)

 

BRTSPClient http://www.live.com/liveMedia/doxygen/html/classRTSPClient.html

  

对于RTSPClient来首,Medium就是一个使用UsageEnvironment变量的一个中转站,或者说Medium是后面所有派生类的中转站^_^

RTSPClient主要有以下几个功能

DescribeURL()  //This is the client first used function to determine if the appoint URL is validate

Send OPTION ANNOUNCE DESCRIBE request到主机

setupMediaSubsession

playMediaSession

playMediaSubSession

pauseMediaSubsession

pauseMediaSession

recordMediaSubsession

setMediaSessionParameter

teardownMediaSubsession

teardownMediaSession

以上这些是public函数,同时RTSPClient还有以下一些内部函数,作为与Server之间的通信信息交互

sendRequest

getResponse

parseResponseCode  parseTransportResponse  parseRTPInfoHeader  parseScaleHeader

而对于RTSP session建立过程中server发过来的SDP信息部分,live的代码里则放在了MediaSession类中进行处理了,这也是符合实际情况的,因为Session以及SubSession的建立初始信息就是依赖SDP信息的。

上面的函数功能就是发送RTSP规定的几种request到目的URL中,得到不同的反馈,比较重要的是DESCRIBE,它的responseserver给的SDP信息

BTWRTSPClient中与Server的连接是基于TCP的,因此。。。

 

C. RTCPInstance

RTCP本分其实是脱离不了RTP连接的,它的端口是对应的RTP socket连接端口+1,同时rtp数据包的到来都会引起RTCP内部(统计)数据的变化

 

 

 

D. MediaSession  (http://www.live.com/liveMedia/doxygen/html/classMediaSession.html)

 

需要说明的是,MediaSubSession才是基本的构成单位,MediaSession类只是对下面所属的SubSession做了一个统一管理,使用了一个Iterator

 

D.1 MediaSession

session做总体控制,例如得到session的起始结束时间,session播放的scale以及Seek

另外初始化整个mediasession,使用RTSPClient传来的SDP信息(必然需要parse SDP包含的各种信息)

 

D.2 MedaiSubSession

则是实现每个独立媒体的控制

m=之下的SDP信息详细parse 得到单个media的特殊信息,同时与Groupsock, *Source以及*Sink建立关联,而后两者分别为处理receive以及sinkRTP packet数据

 

 

E. MediaSink

 

 

 

 

FMediaSource   FrameSource

 

class MediaSource:: public Medium

class FramedSource: public MediaSource

这里将它们放在一起是因为我觉得它们可以合并在一起,(BTW:我就是这么做的)

该类是整个数据读取处理的中转站,同时也是下面函数的基类,定义了一些纯虚函数,供后面调用(例如 MultiFramedRTPSource),FrameSourceMultiFramedRTPSource以及*Sink组成了一个完整的frame数据处理循环。

下面详细介绍FrameSource实现的函数

void FrameSource::getNextFrame(unsigned char* to, unsigned maxSize,

                                                                    afterGettingFunc* afterGettingFunc,

                                                                    void* afterGettingClientData,

                                                                    onCloseFunc* onCloseFunc,

                                                                    void* onCloseClientData)

// Common for all the frame source class, so defined it here, used by the *Sink??

这个函数传递数据buffer指针以及大小,以及后期处理函数(得到数据后传给谁来处理 结束时如何处理),

{

         // Make sure we aren't already being read;

         if(fIsCurrentlyAwaitingData)

         {

envir() << "FramedSource[" << this << "]::getNextFrame(): attempting to read more than once at the same time!/n";

                   exit(1);

         }

 

         fTo = to;                                                   // Input buffer pointer

         fMaxSize = maxSize;                                // Left buffer size

         fNumTruncatedBytes = 0;                       // default value, can be changed by doGetNextFrame();

         fDurationInMicroseconds = 0;       // default value, can be changed by doGetNextFrame();

         fAfterGettingFunc = afterGettingFunc;

         fAfterGettingClientData = afterGettingClientData;

         fOnCloseFunc = onCloseFunc;

         fOnCloseClientData = onCloseClientData;

         fIsCurrentlyAwaitingData = True;

 

         doGetNextFrame();                                  // absolutly virtual function, will be difineed.

}

同时将会调用doGetNextFrame()来处理下一帧数据,这里调用的MultiFramedRTPSource中的处理函数

 

下面的函数将在得到一帧数据后如何处理,这里将函数指到了上面得到的处理函数指针的地址上,live.com的代码则是在*Sink中。

// After the whole frame(MAU) has been got

void FramedSource::afterGetting(FramedSource* source) 

{

  source->fIsCurrentlyAwaitingData = False;

      // indicates that we can be read again

      // Note that this needs to be done here, in case the "fAfterFunc"

      // called below tries to read another frame (which it usually will)

  if (source->fAfterGettingFunc != NULL) {

    (*(source->fAfterGettingFunc))(source->fAfterGettingClientData,

                                                                source->fFrameSize, source->fNumTruncatedBytes,

                                                               source->fPresentationTime,

                                                               source->fDurationInMicroseconds);

  }

}

这里,将处理完一帧数据作为可以读写socket的判断标志之一,主要是因为怕读写过于频繁或者过少早成资源浪费或者是数据处理不及时的情况,我觉得这里可以变动下,因为现在的视频媒体很多是一帧放在几个packet中的,所以。。。

 

对于停止得到帧数据的处理,在FrameSource的派生类中有作用,而停止整个线程,则由handleColsure来处理,见下面函数,仍然调用初始时候传进来的函数指针来作用。

void FrameSource::stopGettingFrames()

{

         fIsCurrentlyAwaitingData = False;

         doStopGettingFrames();

}

void FrameSource::doStopGettingFrames()

{

         // Default implementation: Do nothing

         // Subclasses may wish to specialize this so as to ensure that a

         // subsequent reader can pick up where this one left off.

}

 

void FrameSource::handleColsure(void* clientData)  //This should be called if the source is discovered to be closed(i.e.,no longer readable)

{

         FrameSource* source = (FrameSource*)clientData;

         source->fIsCurrentlyAwaitingData = False;              //We now got a close instead

         if(source->fOnCloseFunc != NULL)

         {

                   (*(source->fOnCloseFunc))(source->fOnCloseClientData);

         }

}

 

 

 

 

 

2.3.3.2 RTP payload format部分介绍

三、一些总结

A. Buffer 管理

How to control the burst input packet is a big topic. The leak bucker model may be useful, however, if a long burst of higher-rate packets arrives (in our system), the bucket will overflow and our control function will take actions against packets in that burst.

 

In our client system, in order to get the library (manager the session and the receiving thread) and the player (used to display picture and put the sound to the sound box, and this place include the decoder), we put a middle layer between the Server and the Player, which is easy for porting.

The following gives a more detailed description.

 

BTW: the UPC (usage parameter control) and the process of handling the exception, such as packet-loss, are complicated and we will not give a full description here.

1.       Receiver Buffer

When the session has been set, we will be ready for receiving the streaming packet. Now, for example, there are two media subsessions which one is Audio subsession and the other one is Video subsession, and we have one buffer for each of them, the following is the details of the receiver buffer manager.

1)      In the receive part, we have defined a ‘Packet’ class, which used to store and handle one RTP packet.

2)      For each subsession, there is one buffer queue whose number is variable, and according to the Maxim delay time, we determine the number of the buffer queue.

3)      Buffer queue is responsible for the packet re-order and something else.

4)      In the receiver buffer, we will handle the packet as soon as possible (except one packet is delay by the network, and we will wait for it until arrived the delay threshold), and leave the buffer overflow and underflow manager to the Player.

Figure 1: packet receive flow

 

Figure 2: packet handle flow (with the decoder)

 

2.       Player (Decoding) Buffer

The player stores media data from the RTSP client into buffers for every stream. The player allocates memory for every stream according to the maximum preroll length. In the initial phase, the player will wait for buffering till every stream has received contents at least Preroll time. So every buffer length will be Prerollmax + C (here C is a constant). When every buffer is ready, the player will start the playback thread and play the contents.

Figure 3: Playback with Stream Buffers

 

The playback thread counts time stamps for every stream. During playing process, one of the streams may be delayed and then the corresponding buffer will under run. If the video stream is delayed, the audio will play normally but the video stalls. The play back thread will continue to count time stamp for audio stream but the video time stamp will not increase. When the new video data is arrived the play back thread will decide it should skip some video frames till the next key frame or play faster to catch the audio time stamp. Usually the player may choose playing faster if it’s just delayed a short time. On the other hand, if it’s the audio stream that is stalled, the player will buffer again till every buffer stores data more than T time. Here T is a short time related with the audio stream’s preroll time, and it can be smaller or equal to the preroll. This dealing is for reducing discontinuity of audio when network is jitter. To avoid this case, choose a higher T value or choose a better network.

 

If one of the buffers is overflow, this is treated as an error. For the video stream, the error handler will drop some data till next key frame arrives. And for audio stream, the error handler will simply drop some data.

Figure 4: Process Buffer Overflow or Underflow

B. How to control the receive loop

liveopenRTSP代码的主循环

env->taskScheduler().doEventLoop()

中,函数doEventLoop有一默认的参数,可以通过设置这个参数达到推出循环的目的,不过可以直接调用下面CD所写的释放资源的方法pause接收或者推出整个线程。

 

C. PAUSE&SEEK

OpenRTSP例子没有给具体的实现,最新的livemedia版本可以支持SEEK了(包括服务器部分)

//PAUSE

        playerIn.rtspClient->pauseMediaSession(*(playerIn.Session));

playerIn.rtspClient->playMediaSession(*(playerIn.Session), -1); 

//will resume

 

// SEEK

 float SessionLength = Session->playEndTime()

     //先得到播放时间区域,在SDP解析中

 先PAUSE***

 再rtspClient->PlayMediaSession(Session, start); 

       //start less than the "SessionLength "

D. 释放资源问题

OpenRTSP给出的解决方案是shutdown()函数,而在我们将库与播放器连接过程中,发觉有线程始终不能推出,后来参考Mplayer(它的rtsp支持采用的就是live的代码)的释放方案,给出以下代码,目前运行一切正常。

void OutRTSPClient() //rtpState是我们定义的一个数据结构体,保存了一些会话信息

{

    if (rtpState->Session == NULL)

       return;

    if (rtpState->rtspClient != NULL) {

       MediaSubsessionIterator iter(*(rtpState->Session));

       MediaSubsession* subsession;

       while ((subsession = iter.next()) != NULL) {

           Medium::close(subsession->sink);

            subsession->sink = NULL;

                                                rtpState->rtspClient->teardownMediaSubsession(*subsession);

       }

    }

    

    UsageEnvironment* env = NULL;

    TaskScheduler* scheduler = NULL;

    if (rtpState->Session != NULL) {

       env = &(rtpState->Session->envir());

       scheduler = &(env->taskScheduler());

    }

    Medium::close(rtpState->Session);

    Medium::close(rtpState->rtspClient);

    

    env->reclaim();

    delete scheduler;

}

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/da5le/archive/2004/12/10/211970.aspx

live555源代码简介

liveMedia项目的源代码包括四个基本的库,各种测试代码以及IVE555 Media Server

四个基本的库分别是UsageEnvironment&TaskSchedulergroupsockliveMediaBasicUsageEnvironment

UsageEnvironmentTaskScheduler类用于事件的调度,实现异步读取事件的句柄的设置以及错误信息的输出。另外,还有一个HashTable类定义了一个通用的hash表,其它代码要用到这个表。这些都是抽象类,在应用程序中基于这些类实现自己的子类。

groupsock类是对网络接口的封装,用于收发数据包。正如名字本身,Groupsock主要是面向多播数据的收发的,它也同时支持单播数据的收发。Groupsock定义了两个构造函数

    Groupsock(UsageEnvironment& env, struct in_addr const& groupAddr,

              Port port, u_int8_t ttl);

    Groupsock(UsageEnvironment& env, struct in_addr const& groupAddr,

              struct in_addr const& sourceFilterAddr,

              Port port);

前者是用于SIMsource-independent multicast)组,后者用于SSMsource-specific multicast)组。groupsock库中的Helper例程提供了读写socket等函数,并且屏蔽了不同的操作系统之间的区别,这是在GroupsockHelper.cpp文件中实现的。

liveMedia库中有一系列类,基类是Medium,这些类针对不同的流媒体类型和编码。

各种测试代码在testProgram目录下,比如openRTSP等,这些代码有助于理解liveMedia的应用。

LIVE555 Media Server是一个纯粹的RTSP服务器。支持多种格式的媒体文件:

      * TS流文件,扩展名ts

      * PS流文件,扩展名mpg

      * MPEG-4视频基本流文件,扩展名m4e

      * MP3文件,扩展名mp3

      * WAV文件(PCM),扩展名wav

      * AMR音频文件,扩展名.amr

      * AAC文件,ADTS格式,扩展名aac。 

live555开发应用程序

基于liveMedia的程序,需要通过继承UsageEnvironment抽象类和TaskScheduler抽象类,定义相应的类来处理事件调度,数据读写以及错误处理。live项目的源代码里有这些类的一个实现,这就是“BasicUsageEnvironment”库。BasicUsageEnvironment主要是针对简单的控制台应用程序,利用select实现事件获取和处理。这个库利用Unix或者Windows的控制台作为输入输出,处于应用程序原形或者调试的目的,可以用这个库用户可以开发传统的运行与控制台的应用。 

通过使用自定义的“UsageEnvironment”“TaskScheduler”抽象类的子类,这些应用程序就可以在特定的环境中运行,不需要做过多的修改。需要指出的是在图形环境(GUI toolkit)下,抽象类 TaskScheduler 的子类在实现 doEventLoop()的时候应该与图形环境自己的事件处理框架集成。

先来熟悉在liveMedia库中SourceSink以及Filter等概念。Sink就是消费数据的对象,比如把接收到的数据存储到文件,这个文件就是一个SinkSource就是生产数据的对象,比如通过RTP读取数据。数据流经过多个'source''sink's,下面是一个示例:

      'source1' -> 'source2' (a filter) -> 'source3' (a filter) -> 'sink'

从其它Source接收数据的source也叫做"filters"Module是一个sink或者一个filter

数据接收的终点是Sink类,MediaSink是所有Sink类的基类。MediaSink的定义如下:

class MediaSink: public Medium {

public:

    static Boolean lookupByName(UsageEnvironment& env, char const* sinkName,

                                MediaSink*& resultSink);

    typedef void (afterPlayingFunc)(void* clientData);

    Boolean startPlaying(MediaSource& source,

                         afterPlayingFunc* afterFunc,

                         void* afterClientData);

    virtual void stopPlaying();

    // Test for specific types of sink:

    virtual Boolean isRTPSink() const;

    FramedSource* source() const {return fSource;}

protected:

    MediaSink(UsageEnvironment& env); // abstract base class

    virtual ~MediaSink();

    virtual Boolean sourceIsCompatibleWithUs(MediaSource& source);

        // called by startPlaying()

    virtual Boolean continuePlaying() = 0;

        // called by startPlaying()

    static void onSourceClosure(void* clientData);

        // should be called (on ourselves) by continuePlaying() when it

        // discovers that the source we're playing from has closed.

    FramedSource* fSource;

private:

    // redefined virtual functions:

    virtual Boolean isSink() const;

private:

    // The following fields are used when we're being played:

    afterPlayingFunc* fAfterFunc;

    void* fAfterClientData;

};

Sink类实现对数据的处理是通过实现纯虚函数continuePlaying(),通常情况下continuePlaying调用fSource->getNextFrame来为Source设置数据缓冲区,处理数据的回调函数等,fSourceMediaSink的类型为FramedSource*的类成员;

基于liveMedia的应用程序的控制流程如下:

应用程序是事件驱动的,使用如下方式的循环

      while (1) {

          通过查找读网络句柄的列表和延迟队列(delay queue)来发现需要完成的任务

          完成这个任务

      }

对于每个sink,在进入这个循环之前,应用程序通常调用下面的方法来启动需要做的生成任务:

      someSinkObject->startPlaying();

任何时候,一个Module需要获取数据都通过调用刚好在它之前的那个ModuleFramedSource::getNextFrame()方法。这是通过纯虚函数FramedSource:oGetNextFrame()实现的,每一个Source module都有相应的实现。

Each 'source' module's implementation of "doGetNextFrame()" works by arranging for an 'after getting' function to be called (from an event handler) when new data becomes available for the caller.

注意,任何应用程序都要处理从'sources''sinks'的数据流,但是并非每个这样的数据流都与从网络接口收发数据相对应。

比如,一个服务器应用程序发送RTP数据包的时候用到一个或多个"RTPSink" modules。这些"RTPSink" modules以别的方式接收数据,通常是文件 "*Source" modules (e.g., to read data from a file), and, as a side effect, transmit RTP packets. 

一个简单的RTSP客户端程序

在另一个文章里,给出了这个简单的客户端的程序的代码,可以通过修改Makefile来裁剪liveMedia,使得这个客户端最小化。此客户端已经正常运行。

首先是OPTION

然后是DESCRIBE

      建立Media Session,调用的函数是 MediaSession::createNew,在文件liveMedia/MediaSession.cpp中实现。

      为这个Media Session建立RTPSource,这是通过调用 MediaSubsession::initiate来实现的的,这个方法在liveMedia/MediaSession.cpp中实现。

在然后是SETUP

最后是PLAY

rtp数据的句柄:MultiFramedRTPSource::networkReadHandler liveMedia/MultiFramedRTPSource.cpp

rtcp数据处理的句柄:RTCPInstance::incomingReportHandler liveMedia/RTCP.cpp

rtp数据处理的句柄的设置:MultiFramedRTPSource:oGetNextFrame liveMedia/MultiFramedRTPSource.cppFileSink::continuePlaying调用在FileSink.cpp.

rtcp数据处理的句柄设置fRTCPInstance = RTCPInstance::createNew /liveMedia/MediaSession.cpp中调用,

createNew调用了构造函数RTCPInstance::RTCPInstance,这个构造函数有如下调用

TaskScheduler::BackgroundHandlerProc* handler = (TaskScheduler::BackgroundHandlerProc*)&incomingReportHandler;  

*********************************************************************************************************************

通过分析live库提供的例子程序OpenRTSP,可以清晰地了解客户端接收来自网络上媒体数据的过程。注意,RTP协议和RTCP协议接收的数据分别是视音频数据和发送/接收状况的相关信息,其中,RTP协议只负责接收数据,而RTCP协议除了接收服务器的消息之外,还要向服务器反馈。

A.        main函数流程

main(int argc,char *argv[])

{

1.            创建BasicTaskScheduler对象

2.            创建BisicUsageEnvironment对象

3.            分析argv参数,(最简单的用法是:openRTSP rtsp://172.16.24.240/mpeg4video.mp4)以便在下面设置一些相关参数

4.            创建RTSPClient对象

5.            RTSPClient对象向服务器发送OPTION消息并接受回应

6.            产生SDPDescription字符串(由RTSPClient对象向服务器发送DESCRIBE消息并接受回应,根据回应的信息产生SDPDescription字符串,其中包括视音频数据的协议和解码器类型)

7.            创建MediaSession对象(根据SDPDescriptionMediaSession中创建和初始化MediaSubSession子会话对象)

8.            while循环中配置所有子会话对象(为每个子会话创建RTPSourceRTCPInstance对象,并创建两个GroupSock对象,分别对应RTPSourceRTCPInstance对象,把在每个GroupSock对象中创建的socket描述符置入BasicTaskScheduler::fReadSet中,RTPSource对象的创建的依据是SDPDescription,例如对于MPEG4文件来说,视音频RTPSource分别对应MPEG4ESVideoRTPSourceMPEG4GenericRTPSource对象。RTCPInstance对象在构造函数中完成将Socket描述符、处理接收RTCP数据的函数(RTCPInstance::incomingReportHandler)以及RTCPInstance本身三者绑定在一个HandlerDescriptor对象中,并置入BasicTaskScheduler::fReadHandler中。完成绑定后会向服务器发送一条消息。)

9.            RTSPClient对象向服务器发送SETUP消息并接受回应。

10.        while循环中为每个子会话创建接收器(FileSink对象),在FileSink对象中根据子会话的codec等属性缺省产生记录视音频数据的文件名,视音频文件名分别为:video-MP4V-ES-1audio-MPEG4-GENERIC-2,无后缀名

11.        while循环中为每个子会话的视音频数据装配相应的接收函数,将每个子会话中的RTPSource中的GroupSock对象中的SOCKET描述符,置入BasicTaskScheduler::fReadSet中,并将描述符、处理接收RTP数据的函数(MultiFramedRTPSource::networkReadHandler)以及RTPSource本身三者绑定在一个HandlerDescriptor对象中,并置入BasicTaskScheduler::fReadHandler,并将FileSink的缓冲区和包含写入文件操作的一个函数指针配置给RTPSource对象,这个缓冲区将会在networkReadHandler中接收来自网络的视音频数据(分析和去掉RTP包头的工作由RTPSource完成),而这个函数指针在networkReadHandler中被调用以完成将缓冲区中的数据写入文件。

12.        RTSPClient对象向服务器发送PLAY消息并接受回应。

13.        进入while循环,调用BasicTaskScheduler::SingleStep()函数接受数据,直到服务器发送TREADOWN消息给客户端,客户端接收到该消息后释放资源,程序退出。

}

发表于 @ 20080130日 11:07:00 | 评论( 11 ) | 编辑举报收藏 

旧一篇:Compile Live555 using VS2005 | 新一篇:Compiling GStreamer for Windows using MinGW

查看最新精华文章 请访问博客首页相关文章 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/imliujie/archive/2008/01/30/2072657.aspx

你可能感兴趣的:(在Livemedia的基础上开发自己的流媒体客户端)