前一篇文章講解了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_PLAY中。
2)在handleCmd_PLAY函數中,首先查找對應的媒體流,進行播放範圍的偏移,然後調用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的應答命令。
服務器應答命令如下: