LIVE555-媒体流建立(3)
前一篇文章讲解了live555的RTSP的创建流程,接下来我将对流媒体的创建过程进行讲解。
目录
1.基本概念了解
在讲解这些之前,咱们先熟悉一下live555中的一些基本类的概念和相互之间的关系:
MediaServer:媒体服务器。一般每个程序中只有一个媒体服务器,该对象是我们运行程序的最高长官,把控全局,其内部记录了所有的clientsession、CllientConnection和ServerMediaSession等对象,其派生了很多子类,例如上一章的rtspServer、以及在范例中自定义的DynamicRTSPServer等等。
ClientConnection:客户端连接对象。通过上一篇文章,可以知道每当客户端连接上服务器后就会创建一个连接对象,负责与客户端之间的数据交互,此处不在赘述,详细可以参见上篇文字(live555- RTSP创建)。
ServerMediaSession(简称:SMS):流媒体会话。当客户端请求播放某个流媒体(test.ts)时,live555就会查找此媒体会话,若不存在则创建新的流媒体会话。一个媒体(文件或直播码流)对应这一个SMS,SMS在MediaServer中是通过流媒体名称进行区分的,所以每个流媒体文件只有一个SMS,负责处理多个客户端的流媒体发送,每个会话中有一个或多个ServerMediaSubSession(通道),实际负责处理媒体流数据传输的是subSession,SMS相当于一个外壳容器。
ServerMediaSubSession:流媒体子会话(通道)。流媒体子会话可以理解为媒体传输通道,例如视频通道、音频通道等,一个SMS中可能包含多个媒体通道。每个媒体通道负责数据的传输控制。
StreamState:媒体流。媒体流包含媒体发送和包装的RTPsink,以及读取媒体文件或码流的source,媒体流保存在clientsession中,一个subsession的通道下可能存在多个媒体流(一个客户端会话对应一个媒体流),也可以设置多个客户端使用一个媒体流。
ClientSession:客户端会话。每当客户端与媒体服务器建立连接(SETUP)后,就会创建一个会话对象,当媒体流传输完成后就会销毁此对象。与网络中的会话机制类似,用户登录后就创建会话,直到登出会话结束。
客户端会话有一个独一无二的sessionId,以确保此会话独一无二,后续与客户端交互都会带着Sessionid。
客户端会话保存着此会话中的媒体流(StramState),通过媒体流就行流媒体产生和推送。
ok 熟悉了上面的概念后,咱们就可以继续进行媒体流创建。后面以VLC作为客户端请求test.264文件,通过wireshark抓包分析媒体流的创建过程。
2.OPTIONS请求
首先客户端发起options请求,询问服务器支持那些请求命令,
客户端请求命令抓包截图如下:
服务器收到信息后进入TSPServer::RTSPClientConnection::handleRequestBytes()函数中,主要进行以下几步:
1)解析请求字符串内容。
2)获得请求命令字符串OPTIONS.
3)进入OPTIONS命令处理函数RTSPClientConnection::handleCmd_OPTIONS(),获得应答字符串。
4)发送应答命令。
服务器应答命令抓包如下:
3.DESCRIBE请求
describe请求从服务器获取流媒体文件格式信息和传输信息,
客户端请求命令抓包如下:
服务器收到信息后进入TSPServer::RTSPClientConnection::handleRequestBytes()函数中,主要进行以下几步:
1)解析请求命令DESCRIBE,进入函数RTSPClientConnection::handleCmd_DESCRIBE(),代码简化如下:
RTSPClientConnection:handleCmd_DESCRIBE(char const* urlPreSuffix,
char const* urlSuffix, char const* fullRequestStr)
{
ServerMediaSession* session = NULL;
//! 1.查找对应的SMS,不存在则终止
session = fOurServer.lookupServerMediaSession(urlTotalSuffix);
//! 2.生成对应的SDP信息,不存在则种植
sdpDescription = session->generateSDPDescription();
//! 3.拼装应答语句
snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,...)
}
在DynamicRTSPServer就是重载了lookupServerMediaSession函数,根据不同的媒体流文件创建对应的媒体流会话(SMS),从而实现文件rtsp服务器,找到SMS后就是通过SMS获取SDP描述。其中generateSDPDescription函数主要用于拼装SDP信息,至于SDP大家可以自行了解,需要关注的是此函数调用了 subsession->sdpLines()产生部分SDP信息。下面列出此函数简化代码:
char const* OnDemandServerMediaSubsession::sdpLines() {
//1.创建临时的源对象
unsigned estBitrate;
FramedSource* inputSource = createNewStreamSource(0, estBitrate);
//2.创建临时的sink对象
RTPSink* dummyRTPSink = createNewRTPSink(dummyGroupsock, rtpPayloadType, inputSource);
//!3.获取SDP信息
setSDPLinesFromRTPSink(dummyRTPSink, inputSource, estBitrate);
//!4.删除临时的source和sink对象
Medium::close(dummyRTPSink);
delete dummyGroupsock;
closeStreamSource(inputSource);
}
此处主要注意的是此处创建的source和sink只是临时的,用完后会立刻删除,主要用于生成SDP信息。
OK,生成SDP信息后,返回describe应答。
- 服务器应答命令如下:
从上图可以看到contorl为通道track1, 其余sdp信息自行学习。
4.SETUP请求
setup请求,用于与服务器建立会话,确定数据流传输方式。
客户端请求令如下:
可以看到此处与前面几个命令不同之处是uri增加了通道信息track1。Transport参数设置了传输模式,包的结构:UDP传输,单播,以及RTP和RTCP客户端所有端口。请求命令讲解到此,接下来是服务器的响应操作:
服务器收到信息后进入TSPServer::RTSPClientConnection::handleRequestBytes()函数中,主要进行以下几步:
1)在rtspServrer中查找clientsession,若未找到则创建CLientSession,其中每个ClientSession会创建一个对应的SessionId,其在rtspServrer中是唯一的,然后跳转到ClientSession的函数handleCmd_SETUP中。
2)接下来我们分析ClientSession的函数handleCmd_SETUP的函数
void RTSPClientSession::handleCmd_SETUP(RTSPServer::RTSPClientConnection* ourClientConnection, char const* urlPreSuffix, char const* urlSuffix, char const* fullRequestStr)
{
//1.根据流名 查找SMS
fOurServerMediaSession = fOurServer.lookupServerMediaSession(streamName,
fOurServerMediaSession == NULL);
//!2.将SMS中所有通道记录到结构体streamState数组中
fNumStreamStates = fOurServerMediaSession->numSubsessions();
fStreamStates = new struct streamState[fNumStreamStates];
ServerMediaSubsessionIterator iter(*fOurServerMediaSession);
ServerMediaSubsession* subsession;
for (unsigned i = 0; i < fNumStreamStates; ++i)
{
subsession = iter.next();
fStreamStates[i].subsession = subsession;
fStreamStates[i].tcpSocketNum = -1;
fStreamStates[i].streamToken = NULL; //在getStreamParameters赋值
}
//3.根据通道信息查找对应的SUbsussesion和trackNum
ServerMediaSubsession* subsession = ....
unsigned trackNum ....
//4.继续解析报文,clientPort,udp或tcp,是否创建后就播放等信息
.........此处主要描述UDP创建的RTP,
Port clientRTPPort(clientRTPPortNum);
Port clientRTCPPort(clientRTCPPortNum);
//5.通过subsession创建对应的媒体流
subsession->getStreamParameters(fOurSessionId, ourClientConnection->
fClientAddr.sin_addr.s_addr,
clientRTPPort, clientRTCPPort,
fStreamStates[trackNum].tcpSocketNum, rtpChannelId, rtcpChannelId,
destinationAddress, destinationTTL, fIsMulticast,
serverRTPPort, serverRTCPPort,
fStreamStates[trackNum].streamToken);
//6.拼装应答文本
}
通过上述代码及本文开头类图,可以看出其中存在一个streamState结构体和StreamState类,注意区分,在streamState结构体中的一个成员streamToken指向StreamState对象指针,结构体声明如下:
struct streamState {
ServerMediaSubsession* subsession;
int tcpSocketNum;
void* streamToken;
} * fStreamStates;
言归正传,clientSession在解析handleCmd_SETUP函数中,首先找到对应的sms和subsession,然后通过subsession的函数getStreamParameters()对媒体流信息进行赋值(创建),可以看到该函数在基类中是纯虚函数,真正的实现实在子类中实现
接下来进入OnDemandServerMediaSubsession的重载函数,其简化代码如下所示:
void OnDemandServerMediaSubsession
::getStreamParameters(unsigned clientSessionId,
netAddressBits clientAddress,
Port const& clientRTPPort,
Port const& clientRTCPPort,
int tcpSocketNum,
unsigned char rtpChannelId,
unsigned char rtcpChannelId,
netAddressBits& destinationAddress,
u_int8_t& /*destinationTTL*/,
Boolean& isMulticast,
Port& serverRTPPort,
Port& serverRTCPPort,
void*& streamToken) {
//!1.如果重用流则直接返回上次的流对象的信息
if (fLastStreamToken != NULL && fReuseFirstSource) {
// Special case: Rather than creating a new 'StreamState',
// we reuse the one that we've already created:
serverRTPPort = ((StreamState*)fLastStreamToken)->serverRTPPort();
serverRTCPPort = ((StreamState*)fLastStreamToken)->serverRTCPPort();
++((StreamState*)fLastStreamToken)->referenceCount();
streamToken = fLastStreamToken;
}
else {
//!2.创建此ClientSession的Source
FramedSource* mediaSource
= createNewStreamSource(clientSessionId, streamBitrate);
//!3.创建SINk
rtpSink = createNewRTPSink(rtpGroupsock, rtpPayloadType, mediaSource);
if (rtpSink != NULL && rtpSink->estimatedBitrate() > 0) streamBitrate = rtpSink->estimatedBitrate();
}
// 4.创建媒体流StreamState
streamToken = fLastStreamToken
= new StreamState(*this, serverRTPPort, serverRTCPPort, rtpSink, udpSink,
streamBitrate, mediaSource,
rtpGroupsock, rtcpGroupsock);
}
}
通过上述代码可以看出,ClientSession中保存着对应流媒体的stramState数组,其中Clientsession使用的通道streamState.streamToken对象是隶属于此Clientsession会话的,如果未设置公用则,没个会话都会创建一个Source,一个SINK,一个StreamState(其中记录source和SINK)信息,至于sink和source我会在后续文章中讲解。
创建媒体流后,就基本完成setup信息了,回退到上一个函数进行应答信息拼装。
服务器应答命令如下:
Transport:说明UDP方式,非多播,目标地址,源地址以及RTP、RTCP的客户端端口和服务的端口
Session:ClientSessionID和超时信息。
5.PLAY请求
play请求,用于请求服务器播放视频,开始媒体流传输。
客户端请求命令如下:
可以看到客户端发送PLAY命令中带着sessionid信息,Range是播放时间段。
服务器收到信息后进入TSPServer::RTSPClientConnection::handleRequestBytes()函数中,主要进行以下几步:
1)在rtspServrer中查找clientsession,然后跳转到ClientSession的函数handleCmd_SETUP中。
2)在handleCmd_SETUP函数中,首先查找对应的媒体流,进行播放范围的偏移,然后调用fStreamStates[i].subsession->startStream开始播放,最终跳转道void StreamState::startPlaying函数中,其代码如下:
void StreamState
::startPlaying(Destinations* dests, unsigned clientSessionId,
TaskFunc* rtcpRRHandler, void* rtcpRRHandlerClientData,
ServerRequestAlternativeByteHandler* serverRequestAlternativeByteHandler,
void* serverRequestAlternativeByteHandlerClientData) {
if (dests == NULL) return;
//!1.创建RTCP实例
fRTCPInstance = fMaster.createRTCP(fRTCPgs, fTotalBW, (unsigned char*)fMaster.fCNAME, fRTPSink);
// Note: This starts RTCP running automatically
fRTCPInstance->setAppHandler(fMaster.fAppHandlerTask, fMaster.fAppHandlerClientData);
}
//!2.TCP处理
//!3.设置目标地址
//!4.发送RTCP信息
if (fRTCPInstance != NULL) {
// get RTCP-synchronized presentation times immediately:
fRTCPInstance->sendReport();
//!5.开始播放媒体流
fRTPSink->startPlaying(*fMediaSource, afterPlayingStreamState, this);
fAreCurrentlyPlaying = True;
}
PRTsink播放视频不再本文讲述之内,后续文章会继续讲解,至此服务器就开始推流了,后面快进、停止等命令就不在详解了基本一致,最后列出play的应答命令。
服务器应答命令如下:
推荐阅读