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

桂堂东

xiaoguizi@gmail.com

2004-10

                                                                      2004-12 

 

 

 

 

 

 

 

 

友情申明:

本文档适合已经从事流媒体传输工作或者对网络传输协议(特别是 RTSP/RTP/SDP )了解的 xdjm 查看,并且本文档处于最原始阶段,将在近期进行完善,希望大家给出意见和建议

 

如有转载,请注明出处。

Copyright@2004

 


一、背景

       如今流媒体无处不在,而主流流媒体服务器为 Realworks Windows Media Server Apple 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

  2 set 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…

3 Handle 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 格式的,这里每一幀称呼为 MAU Media Access Unit ),而 MAU RTP 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 亦或者 MAU fragment ,而在 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 以及 livemedia 4 个部分,其中 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)

MediaSubsession initiate () 时候使用 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 参数的头文件,以及关于 SSM special 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. 层次介绍

 

A. Medium

 

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

class Medium

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

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

 

B RTSPClient 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 ,它的 response server 给的 SDP 信息

BTW RTSPClient 中与 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 以及 sink RTP packet 数据

 

 

E. MediaSink

 

 

 

 

F MediaSource   FrameSource

 

class MediaSource :: public Medium

class FramedSource : public MediaSource

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

该类是整个数据读取处理的中转站,同时也是下面函数的基类,定义了一些纯虚函数,供后面调用(例如 MultiFramedRTPSource ), FrameSource MultiFramedRTPSource 以及 *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

live openRTSP 代码的主循环

env->taskScheduler().doEventLoop()

中,函数 doEventLoop 有一默认的参数,可以通过设置这个参数达到推出循环的目的,不过可以直接调用下面 C D 所写的释放资源的方法 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;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是MATLAB调用rtsp的示例代码: ```matlab url = 'rtsp://username:password@ip_address:port/path'; % RTSP URL vidObj = VideoReader(url); % 创建VideoReader对象 while hasFrame(vidObj) img = readFrame(vidObj); % 读取视频帧 imshow(img); % 显示视频帧 end ``` 对于C++ RTSP/RTP流媒体客户端,可以使用开源的Live555库。以下是示例代码: ```cpp #include <liveMedia.hh> #include <BasicUsageEnvironment.hh> #include <stdio.h> int main(int argc, char** argv) { TaskScheduler* scheduler = BasicTaskScheduler::createNew(); UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler); RTSPClient* rtspClient = RTSPClient::createNew(*env, "rtsp://username:password@ip_address:port/path", 0, "username:password"); if (rtspClient == NULL) { fprintf(stderr, "Failed to create RTSP client\n"); exit(1); } RTSPStream* rtspStream = rtspClient->streamingOverTCP() ? (RTSPStream*)new TCPStreamSink(*env) : (RTSPStream*)new UDPStreamSink(*env); if (rtspStream == NULL) { fprintf(stderr, "Failed to create RTSP stream\n"); exit(1); } rtspClient->sendDescribeCommand(continueAfterDESCRIBE, rtspStream); env->taskScheduler().doEventLoop(); // 进入事件循环 Medium::close(rtspStream); Medium::close(rtspClient); env->reclaim(); delete scheduler; return 0; } void continueAfterDESCRIBE(RTSPClient* rtspClient, int resultCode, char* resultString) { if (resultCode == 0) { rtspClient->sendSetupCommand(continueAfterSETUP, False, False); } else { fprintf(stderr, "Failed to send DESCRIBE command: %s\n", resultString); exit(1); } } void continueAfterSETUP(RTSPClient* rtspClient, int resultCode, char* resultString) { if (resultCode == 0) { rtspClient->sendPlayCommand(continueAfterPLAY); } else { fprintf(stderr, "Failed to send SETUP command: %s\n", resultString); exit(1); } } void continueAfterPLAY(RTSPClient* rtspClient, int resultCode, char* resultString) { if (resultCode != 0) { fprintf(stderr, "Failed to send PLAY command: %s\n", resultString); exit(1); } } ``` 需要注意的是,在使用Live555库时需要将其编译后才能使用。具体编译方法可以参考官方文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值