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

一、背景

二、Livemedia框架介绍

1.总体框架

2.客户端框架

2.1 客户端openRTSP流程

2.2增加一种新的媒体

2.2.1增加媒体的format

2.2.2 新媒体需要考虑的问题

2.3类详细说明

2.3.1 BasicUsageEnvironment, UsageEnvironment

2.3.2 groupsock

2.3.3 livemedia

三、一些总结

A. Buffer 管理

B. How to control the receive loop

C. PAUSE&SEEK

D. 释放资源问题

一、背景

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

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;

时间: 2024-10-28 20:24:14

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

在cocos code ide的基础上构建自己的lua开发调试环境

现在无论是端游还是手游(基于cocos2d)的开发, 基本模式都是c++内核 + lua逻辑的路线.对于c++的开发和调试, vs已经能很好的满足开发者的需求, 微软这方面真是值得称颂.不幸的是, lua的开发和调试,还没有一个统一的,方便的开发调试环境,这也给广大的luaer和项目造成了极大的困扰.目前通常的做法大致有如下几种: 1.纯粹意义上的lua编辑器, 如Sublime Text 2 + 插件的模式,解决了代码提示和补齐问题. 2.编辑器的基础上加入调试功能, 通常是直接在vs上开发一

JavaScript基础-前端开发

理解作用域 理解作用域链是Js编程中一个必须要具备的,作用域决定了变量和函数有权力访问哪些数据.在Web浏览器中,全局执行环境是window对象,这也意味着所有的全局变量或者方法都是window对象的属性或方法.当一个函数在被调用的时候都会创建自己的执行环境,而这个函数中所写的代码就开始进入这个函数的执行环境,于是由变量对象构建起了一个作用域链. var wow = '魔兽世界'; var message = function(){ var _wow = '123'; } 在这个例子中全局环境中

【linux基础】17、TCP/IP网络基础(上)

一.计算机网络概述 1.什么是网络? 一群具有独立功能的计算机组成的系统通过通信设备及传输媒体互连起来,在通信软件支持下,实现资源共享.信息交换的整个系统 产生计算机网络的基本条件:通信技术与计算机技术的结合 2.计算机网络的产生与发展 1954年 主机-终端的通信网络  第一代计算机网络 以单个主机为中心.面向终端设备的网络结构.系统中除主计算机具有独立的数据处理功能外,系统中所连接的终端设备均无独立处理数据的功能 1969年 主机-主机的通信网络  ARPANET网  第二代计算机网络 以分

最佳的项目一定建立在最佳的软件工程基础上的

快速软件开发有几个基本原则:管理原则.技术原则.质量保证原则. 其中和我目前关系较深的是技术原则. 作者在第四章的前段便引用了这句话“最佳的项目一定建立在最佳的软件工程基础上的”. 而我认为我计算机相关的基础都比较薄弱,作者并没有给出软件工程基础相关的推荐书籍. 不知道是否有一些软件工程基础的推荐书籍. 最好是能够相互对照的.

iOS开发UI基础—IOS开发中Xcode的一些使用技巧

iOS开发UI基础-IOS开发中Xcode的一些使用技巧 一.快捷键的使用 经常用到的快捷键如下: 新建 shift + cmd + n     新建项目 cmd + n             新建文件 视图 option + cmd + 回车 打开助理编辑器 cmd + 回车           显示主窗口 cmd + 0             导航窗口 option + cmd + 0    工具窗口 在.m & .h之间切换           control + cmd + 上/下

二、Photon v4引擎基础开发框架——开发第一个Photon程序

Photon基础开发框架 Photon (v4)的基本框架.开发框架主要Photon和游戏逻辑(C#)两个部分,如下图最新的Photon v4支持的4种底层协议,游戏开发逻辑Photon目前主要划分为Load Balancing 和MMO(大型多人同时在线游戏). 一.Photon服务端示例 1.服务端开发 新建解决方案TestPhotonServer并新建类库项目MyPhotonServer,类库添加Photon引用(可在photon安装目录的lib里找到) Photon.SocketServ

Spring基础-上-概念篇

*什么是IoC(Inversion of Control)? 即控制反转.其思想是控制资源获取的方向.传统的资源获取的方式为向容器发起请求,然后容器返回资源,而应用了IOC之后,容器主动地将资源退送给它所管理的组件,组件要做的仅仅是选择一种合适的方式来接收资源即可. 买菜的例子. *什么是DI(Dependency Injection)? 即依赖注入.通常来讲,一个复杂的应用需要多个类相互协作,譬如类A要实现功能A需要类B的协作,那么类A就是依赖于类B,传统的做法是在类A的内部直接创建B的对象,

如何在响应式基础上提升移动性能///响应式不是万能的!教你提升响应式设计的移动性能(一)

如何在响应式基础上提升移动性能 摘要:如何在响应式基础上提升移动性能,从细节做起,结合网站,做好响应式页面的设计优化工作.,随着互联网的高速发展,合肥网站建设小编今天为大家介绍,为解决移动性能的响应式页面设计并不是万能的,而应该不断的改进,从而更好的为用户服务.随着互联网的高速发展,例如近段时间炒得火热的谷歌申请的无人机技术,以及电商门户网站阿里巴巴的上市等等,这都促进了互联网日新月异的变化. 所以作为网站seo人员不应固步自封,而应该努力的跟上时代的步伐,不仅需要学习相关的seo知识,而且对于

新书:Scala语言基础与开发实战

大数据科学丛书系列的最新一本<Scala语言基础与开发实战>即将面市,预计月底上架.内容还是不错的,文笔简介,内容实用,值得学.用. 大数据资深培训师王家林新作. 详细介绍大数据开发语言Scala及其在分布式框架Akka和Kafka中的应用. 秉承"实战"类图书特点,解析大量代码的编写操作,具有较强的可操作性,便于读者学习和理解. 算上再过几个月出版的<Spark内核解析及性能调优>,一年时间,大数据科学丛书系列之Spark的小套系基本形成了:从学习Spark的