上一篇Live555原始碼解析(3) - 服務開啟,願者上鉤中我們講到RTSPServer建立後,帶來了兩項重要支援(旁註類比):
- incomingConnectionHandler服務 - 掛著魚餌的釣鉤
- 雜湊表
- ServerMediaSessions - 釣竿與手之間所有互動
- ClientConnections - 釣線
- ClientSessions - 釣鉤與魚之間所有互動
整理一下,其實伺服器已經擺好姿勢,嚴陣以待第一個吃魚餌的魚上鉤了,那麼接下來,我們就看魚兒是怎麼一步一步上鉤來。
1. 魚
釣魚要有魚,服務要有被服務者,Live555媒體伺服器的服務物件就是支援RTSP/RTP協議的客戶端。從官網對客戶端介紹中我們可以看到,目前支援如下主流客戶端:
- VLC media Player
- QuickTime Player
- Amino set-top boxes(只支援MPEG TS流)
- openRTSP命令列客戶端(只能接收/儲存流資料,不支援播放)
要釣就釣大魚,本篇就採用VLC播放器作為客戶端,來探測一下咬鉤引起的連鎖反應。
2. 鉤
先看一下伺服器準備好後的命令列提示介面,如下圖所示:
關鍵幾個資訊,說明如下:
-
入口 Play URL
Play streams from this server using the URL rtsp://192.168.56.1/<filename> where <filename> is a file present in the current directory. 複製程式碼
從中我們可以得到幾個資訊:
- rtsp 代表使用的是TCP作為傳輸層
- 192.168.53.1 表示的是伺服器所在主機IP地址,未顯式給出埠號8554,說明使用了知名埠554
- 檔案必須與存放在程式當前目錄
-
支援檔案型別
- .264 / .265
- .aac / .ac3 / .amr / .mp3 / .ogg / .wav
- .dv / .m4e / .mkv / .mpg / .ts / .vob / .webm
因此,本篇中我們以ts檔案型別為例,將bipbop-gear1-all.ts檔案置於live555MediaServer可執行檔案同一路徑下。對於VLC而言,想要播放(點播)該檔案,則其入口為:
rtsp://192.168.56.1/bipbop-gear1-all.ts
複製程式碼
這也就是魚所看到的鉤,而同時,伺服器正處於doEventLoop()
的迴圈等待中,正如河邊靜氣凝神握著釣竿的手。
3. 來
如圖所示,VLC客戶端開啟網路串流rtsp://192.168.56.1/bipbop-gear1-all.ts
,開始咬鉤。
果不其然,這觸發了doEventLoop()所呼叫的BasicTaskScheduler::SingleStep()中的如下程式碼。
int selectResult = select(fMaxNumSockets, &readSet, &writeSet, &exceptionSet, &tv_timeToDelay);
if(selectResult <0)
{
if( GetLastError() != EINTR )
{
// 異常錯誤,視為嚴重故障;列印錯誤資訊後退出
print_Set_info();
abort();
}
}
else //if(selectResult <0)
{
HandlerIterator iter(*fHandlers);
HandlerDescriptor* handler;
if(fLastHandledSocketNum >= 0)
{
// 如已處理過socket讀寫,則找到前次socket讀寫的下一個連結串列節點
while((handler = iter.next()) != NULL)
{
if(handler->socketNum == fLastHandledSocketNum) break;
}
if(handler == NULL)
{
// 未找到,重置相關值
fLastHandlerSocketNum = -1;
iter.reset();
}
}
while((handler = iter.next()) != NULL)
{
// 找到連結串列中合法節點,開始處理
int sock = handler->socketNum;
int resultConditionSet = 0;
if(FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet)) resultConditionSet |= SOCKET_READABLE;
if(FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet) resultConditionSet |= SOCKET_WRITEABLE;
if(FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet) resultConditionSet |= SOCKET_EXCEPTION;
if((resultConditionSet&handler->conditionSet) != 0 && handler->handlerProc != NULL)
{
// 儲存當前處理節點socketNum
fLastHandledSocketNum = sock;
(*handler->handlerProc)(handler->clientData, resultConditionSet);
break;
}
} // while((handler = iter.next()) != NULL)
...
}
複製程式碼
程式碼已於 Live555原始碼解析(1) - Main 尋根問祖,留其筋骨中Section 3進行了詳細說明,這裡不再贅述。總之要注意的是,incomingConnectionHandler服務已經註冊好,存放位置就是HandlerSet中(詳見Live555原始碼解析(3) - 服務開啟,願者上鉤 Section 2.1.1.1.4)。
3.1 incomingConnectionHandler
有朋自遠方來,GenericMediaServer::incomingConnectionHandler終於粉墨登場。
void GenericMediaServer::incomingConnectionHandler(void* instance, int /*mask*/)
{
GenericMediaServer* server = (GenericMediaServer*)instance;
server->incomingConnectionHandler();
}
複製程式碼
這裡的instance歸根結底註冊時是在GenericMediaServer建構函式中用的this指標,因此呼叫的也就依然是自身無參的incomingConnectionHandler()。
void GenericMediaServer::incomingConnectionHandler()
{
incomingConnectionHandlerOnSocket(fServerSocket);
}
複製程式碼
還是一層封裝,為了類介面的隱藏。
void GenericMediaServer::incomingConnectionHandlerOnSocket(int serverSocket)
{
//@3.1.1 socket accept
struct sockaddr_in clientAddr;
SOCKLEN_T clientAddrLen = sizeof clientAddr;
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
if(clientSocket < 0)
{
int err = envir().getErrno();
if(err != EWOULDBLOCK)
envir().setResultErrMsg("accept() failed: ");
return;
}
//@3.1.2 socket revise
ignoreSigPipeOnSocket(clientSocket);
makeSocketBlocking(clientSocket);
increaseSendBufferTo(envir(), clientSocket, 50*1024);
//@3.1.3 createNewClientConnection
(void)createNewClientConnection(clientSocket, clientAddr);
}
複製程式碼
@3.1.1 socket accept
這段程式碼其實並無多少好說的,如果你看過關於socket程式設計的書,那麼這些就只是基礎的socket accept套路。甚至,如果有需要的,比如顯示客戶端地址、埠資訊,你也可以在套路上加上一些輸出操作。
@3.1.2 socket revise
和Live555原始碼解析(3) - 服務開啟,願者上鉤 中介紹過的一樣,忽略SIGPIPE是為了防止退出,非阻塞模式是為了支援同時多Socket,調整Buffer是為了配合重傳。有興趣的話可以詳細閱讀Live555原始碼解析(3) - 服務開啟,願者上鉤 Section @1.1部分。
@3.1.3 createNewClientConnection
到了這裡,才是真正的重頭戲。這裡實際呼叫的是RTSPServerSupportHTTPStreaming中的createNewClientConnection()
,其程式碼如下:
GenericMediaServer::ClientConnection*
RTSPServerSupportingHTTPStreaming::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr)
{
return new RTSPClientConnectionSupportingHTTPStreaming(*this, clientSocket, clientAddr);
}
複製程式碼
其呼叫了RTSPClientConnectionSupportingHTTPStreaming建構函式。
RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming
::RTSPClientConnectionSupportingHTTPStreaming(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
: RTSPClientConnection(ourServer, clientSocket, clientAddr)
, fClientSessionId(0), fStreamSource(NULL), fPlaylistSource(NULL), fTCPSink(NULL)
{}
複製程式碼
進一步呼叫了RTSPClientConnection()建構函式。
RTSPServer::RTSPClientConnection
::RTSPClientConnection(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
: GenericMediaServer::ClientConnection(ourServer, clientSocket, clientAddr)
, fOurRTSPServer(ourServer), fClientInputSocket(fOurSocket)
, fClientOutputSocket(fOurSocket), fIsActive(True)
, fRecursionCount(0), fOurSessionCookie(NULL)
{
resetRequestBuffer();
}
複製程式碼
resetRequestBuffer()真的就只是重設了請求Buffer(),沒有其他操作。我們需要關注的是這裡呼叫了GenericMediaServer::ClientConnection()函式。
GenericMediaServer::ClientConnection
::ClientConnection(GenericMediaServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
: fOurServer(ourServer), fOurSocket(clientSocket), fClientAddr(clientAddr)
{
//@3.1.3.1 雜湊表
fOurServer.fClientConnections->Add((char const*)this, this);
resetRequestBuffer();
//@3.1.3.2 新服務incomingRequestHandler
envir().taskScheduler().setBackgroundHandling(fOurSocket,
SOCKET_READABLE|SOCKET_EXCEPTION,
incomingRequestHandler, this);
}
複製程式碼
@3.1.3.1 雜湊表
如果你還記得Live555原始碼解析(3) - 服務開啟,願者上鉤 中有提到,可修改ClientConnections雜湊表的API之一就是ClientConnection()建構函式,那麼這裡就可以推出,咬鉤的動作引起了資料的變化。該變化將永存於伺服器生命週期內,直到有人將其從表中抹去。而抹去也只能由雜湊表的另一API,~ClientConnection()解構函式完成。
也就是說,該連線,將從連線建立開始存在,將於連線銷燬而逝去。
@3.1.3.2 新服務incomingRequestHandler
程式是指令加上資料,資料固然重要,但必須指令將其盤活。程式碼到這裡,開啟了新的服務incomingRequestHandler,從名稱上來看,應該是服務於客戶端發出的RTSP請求,那麼究竟是不是呢?就在下一小節繼續跟蹤進去。
3.2 新服務incomingRequestHandler
還是SingleStep()中那段排程程式碼,換了個主角,戲照樣唱。這次輪到incomingRequestHandler。
void GenericMediaServer::ClientConnection::incomingRequestHandler(void* instance, int /*mask*/)
{
ClientConnection* connection = (ClientConnection*)instance;
connection->incomingRequestHandler();
}
複製程式碼
一層封裝。
void GenericMediaServer::ClientConnection::incomingRequestHandler()
{
struct sockaddr_in dummy;
//@3.2.1 readSocket
int bytesRead = readSocket(envir(), fOurSocket, &fRequestBuffer[fRequestBytesAlreadySeen], fRequestBufferBytesLeft, dummy);
//@3.2.2 handleRequestBytes
handleRequestBytes(bytesRead);
}
複製程式碼
@3.2.1 readSocket
依然呼叫的是GroupsockHelper提供的幫助函式,其內部程式碼如下。
int readSocket(UsageEnvironment& env, int socket,
unsigned char* buffer, unsigned bufferSize,
struct sockaddr_in& fromAddress)
{
SOCKLEN_T addressSize = sizeof fromAddress;
int bytesRead = recvfrom(socket, (char*)buffer, bufferSize, 0,
(struct sockaddr*)&fromAddress, &addressSize);
if(bytesRead < 0)
{
int err = env.getErrno();
if( err == 0 || err == EWOULDBLOCK // Windows
|| err == EAGAIN || err == 111 || err == 113 )// ECONNREFUSED(linux)
{
fromAddress.sin_addr.s_addr = 0;
return 0;
}
socketErr(env, "recvfrom() error: ");
}
else if(bytesRead == 0)
return -1;
return bytesRead;
}
複製程式碼
標準read套路,呼叫了winsock的recvfrom函式,對讀取位元組數進行校驗。要麼錯誤了清場報錯,要麼正確了返回。
稍加註意的是最後一個引數,也就是incomingRequestHandler呼叫中的dummy結構體,其用於存放請求發出者,也就是說客戶端的地址,這裡並沒有實際用處。
@3.2.2 handleRequestBytes
void RTSPServer::RTSPClientConnection::handleRequestBytes(int newBytesRead)
{
int numBytesRemaining = 0;
++fRecursionCount;
do{
RTSPServer::RTSPClientSession* clientSession = NULL;
if(newBytesRead < 0 || (unsigned)newBytesRead>= RequestBufferBytesLeft) {
//讀取失敗,或讀取到錯誤資訊,關閉連線
fIsActive = False;
break;
}
Boolean endOfMsg = False;
unsigned char* ptr = &fRequestBuffer[fRequestBytesAlreadySeen];
if(fClientOutputSocket != fClientInputSocket && numBytesRemaining == 0) {
//去除空白字元
unsigned toIndex = 0;
for(int fromIndex = 0; fromIndex < newBytesRead; ++fromIndex) {
char c = ptr[fromIndex];
if(!(c == '' || c == '\t' || c == '\r' || c == '\n'))
ptr[toIndex++] = c;
}
newBytesRead = toIndex;
//判定為RTSP-over-HTTP tunneling,其中內容可能使用Base64編碼,
//所以此處儘可能使用Base64解碼
unsigned numBytesToDecode = fBase64RemainderCount + newBytesRead;
unsigned numBase64RemainderCount = numBytesToDecode % 4;
numBytesToDecode -= newBase64RemainderCount;
if(numBytesToDecode > 0) {
ptr[newBytesRead] = '\0';
unsigned decodedSize;
unsigned char* decodedBytes = base64Decode((char const*)(ptr-fBase64RemainderCount), numBytesToDecode, decodedSize);
unsigned char* to = ptr - fBase64RemainderCount;\
for(unsigned i = 0; i < decodedSize; ++i)
*to++ = decodedBytes[i];
for(unsigned j=0; j < newBase64RemainderCount; ++j)
*to++ = (ptr-fBase64RemainderCount + numBytesToDecode)[j];
newBytesRead = decodedSize - fBase64RemainderCount + newBase64RemainderCount;
delete[] decodedBytes;
}
fBase64RemainderCount = newBase64RemainderCount;
}
//@3.2.2.1 確保Request訊息完整性
unsigned char* tmpPtr = fLastCRLF + 2;
if(fBase64RemainderCount == 0)
{
if(tmpPtr < fRequestBuffer)
tmpPtr = fRequestBuffer;
while(tmpPtr < &ptr[newBytesRead - 1])
{
//查詢訊息結尾識別符號 <CR><LF><CR><LF>
if(*tmpPtr == '\r' && *(tmpPtr + 1) == '\n')
{
if(tmpPtr - fLastCRLF == 2)
{
endOfMsg = True;
break;
}
fLastCRLF = tmpPtr;
}
++tmpPtr;
}
}
fRequestBufferBytesLeft -= newBytesRead;
fRequestBufferAlreadySeen += newBytesRead;
// 確保Request完整性
if(!endOfMsg) break;
fRequestBuffer[fRequstBytesAlreadySeen] = '\0';
char cmdName[RTSP_PARAM_STRING_MAX];
char urlPreSuffix[RTSP_PARAM_STRING_MAX];
char urlSuffix[RTSP_PARAM_STRING_MAX];
char cseq[RTSP_PARAM_STRING_MAX];
char sessionIdStr[RTSP_PARAM_STRING_MAX];
unsigned contentLength = 0;
fLastCRLF[2] = '\0';
//@3.2.2.2 解析RTSP請求
Boolean parseSucceeded = parseRTSPRequstString((char*)fRequestBuffer, fLastCRLF+2 - fRequestBuffer, cmdName, sizeof cmdName, urlPreSuffix, sizeof urlPreSuffix, urlSuffix, sizeof urlSuffix, cseq, sizeof cseq, sessionIdStr, sizeof sessionIdStr, contentLength);
fLastCRLF[2] = '\r';
Boolean playAfterSetup = False;
if(parseSucceeded){
//如頭中存在Content-Length,則再次校驗訊息完整性
if(ptr + newBytesRead < tmpPtr + 2 + contentLength) break;
Boolean const requestIncludedSessionId = sessionIdStr[0] != '\0';
if(requestIncludedSessionId){
//如頭中存在SessionID,則驗證該會話是否存在,並確認其狀態
clientSession = (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
if(clientSession != NULL) clientSession->noteLiveness();
}
//@3.2.2.3 處理RTSP請求中方法
fCurrentCSeq = cseq;
if(strcmp(cmdName, "OPTIONS") == 0){
if(requestIncludedSessionId && clientSession == NULL)
handleCmd_sessionNotFound();
else
handleCmd_OPTIONS();
}
else if(urlPreSuffix[0] == '\0' && rlSuffix[0] == '*' && urlSuffix[1] == '\0'){
if(strcmp(cmdName, "GET_PARAMETER") == 0)
handleCmd_GET_PARAMETER((char const*)fRequestBuffer);
else if(strcmp(cmdName, "SET_PARAMETER") == 0)
handleCmd_SET_PARAMETER((char const*)fRequestBuffer);
else
handleCmd_notSupported();
}
else if(strcmp(cmdName, "DESCRIBE") == 0){
handleCmd_DESCRIBE(urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
}
else if(strcmp(cmdName, "SETUP") == 0){
Boolean areAuthenticated = True;
if(!requestIncludedSessionId){
// 建立會話
char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX];
urlTotalSuffix[0] = '\0';
if(urlPreSuffix[0] != '\0'){
strcat(urlTotalSuffix, urlPreSuffix);
strcat(urlTotalSuffix, "/");
}
strcat(urlTotalSuffix, urlSuffix);
if(authenticationOK("SETUP", urlTotalSuffix, (char const*)fRequestBuffer))
clientSession = (RTSPServer::RTSPClientSession*)fOurRTSPServer.createNewClientSessionWithId();
else
areAuthenticated = False;
}
if (clientSession != NULL)
clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix,urlSuffix, (char const*)fRequestBuffer);
else
handleCmd_sessionNotFound();
}
else if (strcmp(cmdName, "TEARDOWN") == 0
|| strcmp(cmdName, "PLAY") == 0
|| strcmp(cmdName, "PAUSE") == 0
|| strcmp(cmdName, "GET_PARAMETER") == 0
|| strcmp(cmdName, "SET_PARAMETER") == 0) {
if (clientSession != NULL)
clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
else
handleCmd_sessionNotFound();
}
else if(strcmp(cmdName, "REGISTER") == 0 || strcmp(cmdName, "DEREGISTER") == 0) {
char* url = strDupSize((char*)fRequestBuffer);
if (sscanf((char*)fRequestBuffer, "%*s %s", url) == 1) {
Boolean reuseConnection, deliverViaTCP;
char* proxyURLSuffix;
parseTransportHeaderForREGISTER((const char*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);
handleCmd_REGISTER(cmdName, url, urlSuffix, (char const*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);
delete[] proxyURLSuffix;
} else {
handleCmd_bad();
}
delete[] url;
} else {
handleCmd_notSupported();
}
} else {
// RTSP-over-HTTP tunnel
char sessionCookie[RTSP_PARAM_STRING_MAX];
char acceptStr[RTSP_PARAM_STRING_MAX];
*fLastCRLF = '\0';
parseSucceeded = parseHTTPRequestString(cmdName, sizeof cmdName,urlSuffix, sizeof urlPreSuffix, sessionCookie, sizeof sessionCookie, acceptStr, sizeof acceptStr);
*fLastCRLF = '\r';
if (parseSucceeded) {
// Check that the HTTP command is valid for RTSP-over-HTTP tunneling: There must be a 'session cookie'.
Boolean isValidHTTPCmd = True;
if (strcmp(cmdName, "OPTIONS") == 0) {
handleHTTPCmd_OPTIONS();
} else if (sessionCookie[0] == '\0') {
if (strcmp(acceptStr, "application/x-rtsp-tunnelled") == 0)
isValidHTTPCmd = False;
else
handleHTTPCmd_StreamingGET(urlSuffix, (char const*)fRequestBuffer);
} else if (strcmp(cmdName, "GET") == 0){
handleHTTPCmd_TunnelingGET(sessionCookie);
} else if (strcmp(cmdName, "POST") == 0) {
unsigned char const* extraData = fLastCRLF+4;
unsigned extraDataSize = &fRequestBuffer[fRequestBytesAlreadySeen] - extraData;
if (handleHTTPCmd_TunnelingPOST(sessionCookie, extraData, extraDataSize)) {
fIsActive = False;
break;
}
}
else
isValidHTTPCmd = False;
if (!isValidHTTPCmd)
handleHTTPCmd_notSupported();
else
handleCmd_bad();
send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0);
if (playAfterSetup)
clientSession->handleCmd_withinSession(this, "PLAY", urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
unsigned requestSize = (fLastCRLF+4-fRequestBuffer) + contentLength;
numBytesRemaining = fRequestBytesAlreadySeen - requestSize;
resetRequestBuffer();
if (numBytesRemaining > 0) {
memmove(fRequestBuffer, &fRequestBuffer[requestSize], numBytesRemaining);
newBytesRead = numBytesRemaining;
}
} while (numBytesRemaining > 0);
--fRecursionCount;
if(!fIsActive) {
if(fRecursionCount > 0)
closeSockets();
else
delete this;
}
}
}
}
複製程式碼
@3.2.2.1 確保Request訊息完整性
程式碼用於確保已完整接收Request訊息,判斷標註為是否能檢測到訊息結尾標誌CRLF CRLF
或\r\n\r\n
。如未檢測到,退出迴圈,繼續接收,直到完整為止。
@3.2.2.2 解析RTSP請求
函式parseRTSPRequestString()實現位置在RTSPCommon中,同樣以全域性函式形式存在。由於3.2.2中處理函式眾多,如均一一展開,篇幅將過長過臭。因此這裡僅列出其步驟及示例Request,如有興趣,可自行閱讀相關程式碼。
OPTIONS rtsp://192.168.56.1/bipbop-gear1-all.ts RTSP/1.0
CSeq : 2
User-Agent : LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
複製程式碼
- 跳過request開始處的任何空白字元
- 讀取至下一空白符,所讀取到的內容作為命令名稱,此處為OPTIONS
- 跳過字首為rtsp://或rtsp:/的URL,獲取URL指定的檔名,此處為bipbop-gear1-all.ts
- 查詢'CSeq:'頭,如存在,獲取序號值。此處為2
- 查詢'Session:'頭,如有,獲取其值。此處空缺
- 查詢'Content-Length:'頭,如有,獲取其值。此處空缺
補充說明 User-Agent User-Agent用於標識應用型別、作業系統、軟體版本、開發商等資訊。例如
Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1 Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 複製程式碼
此時如伺服器端對不同型別客戶端有做更優適配,如針對手機、電腦製作不同的網頁佈局,就可以更好地提升使用者體驗。
成功解析RTSP請求後,如其中存在SessionID,則需在雜湊表中查詢該ID值。如查詢成功,進一步確認其狀態。
if(requestIncludedSessionId)
{
clientSession = (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
if(clientSession != NULL) clientSession->noteLiveness();
}
複製程式碼
lookupClientSession的原始碼就不放了,純粹的查詢HashTable而已,有興趣的話可以閱讀GenericMediaServer::lookupClientSession()
並進一步跟蹤。
關於noteLiveness()要稍微說明下,因為其可能引申出一個新的延時任務。
void GenericMediaServer::ClientSession::noteLiveness()
{
// 使用預設實現,無其他操作,屬虛張聲勢
if(fOurServerMediaSession != NULL)
fOurServerMediaSession->noteLiveness();
// fReclamationSeconds>0時開啟延時任務livenessTimeoutTask,延時時長為fReclamationSeconds
if(fOurServer.fReclamationSeconds > 0)
envir().taskScheduler().rescheduleDelayedTask(fLivenessCheckTask,
fOurServer.fReclamationSeconds*1000000,
(TaskFunc*)livenessTimeoutTask, this));
}
複製程式碼
fReclamationSeconds是由main函式中DynamicRTSPServer建立時傳遞引數而來,其值為0。因此此處並不會開啟,至於什麼時候會開啟,只能說,本程式中不會開啟。如果開啟,且到達指定時長,則會刪除clientSession。
@3.2.2.3 處理RTSP請求中方法
如對RTSP請求、回覆不太熟悉,可先閱讀Live555原始碼解析(2) - RTSP協議概述。 RTSP請求中會存在幾種方法,這裡列出了所有支援的方法,各方法及相應處理如下:
-
OPTIONS 如果存在會話ID但並未找到相應clientSession,則轉至handleCmd_sessionNotFound(),也就是說回覆"454 未找到會話"訊息。其他方法也有類似處理,就不再一一說明。
如無異常,handleCmd_OPTIONS。void RTSPServer::RTSPCLientConnection::handleCmd_OPTIONS() { snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, "RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n", fCurrentCSeq, dateHeader(), fOurRTSPServer.allowedCommandNames()); } 複製程式碼
標準應答,返回伺服器所支援的所有方法名。示例如下:
RTSP/1.0 200 OK CSeq: 2 Date: Fri, May 26 2017 13:06:44 GMT Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER 複製程式碼
-
DESCRIBE 請求示例:
DESCRIBE rtsp://192.168.56.1/bipbop-gear1-all.ts RTSP/1.0 CSeq: 3 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22) Accept: application/sdp 複製程式碼
handleCmd_DESCRIBE,長話短說:
- 認證檢測,由於預設未啟用認證機制,所以肯定認證通過
- 根據URL中檔名bipbop-gear1-all.ts(如有子目錄,則為完整,如/dir/bipbop-gear1-all.ts)查詢ServerMediaSession。如找到,增加引用計數,如未找到,handleCmd_notFound()也就是回覆"404 未找到流"錯誤後退出
- 生成SDP描述資訊 generateSDPDescription()
回覆示例:
RTSP/1.0 200 OK CSeq: 3 Date: Fri, May 26 2017 13:06:44 GMT Content-Base: rtsp://192.168.56.1/bipbop-gear1-all.ts/ Content-Type: application/sdp Content-Length: 416 v=0 o=- 1495855965038741 1 IN IP4 192.168.56.1 s=MPEG Transport Stream, streamed by the LIVE555 Media Server i=bipbop-gear1-all.ts t=0 0 a=tool:LIVE555 Streaming Media v2017.04.10 a=type:broadcast a=control:* a=range:npt=0- a=x-qt-text-nam:MPEG Transport Stream, streamed by the LIVE555 Media Server a=x-qt-text-inf:bipbop-gear1-all.ts m=video 0 RTP/AVP 33 c=IN IP4 0.0.0.0 b=AS:5000 a=control:track1 複製程式碼
-
SETUP 請求示例:
SETUP rtsp://192.168.56.1/bipbop-gear1-all.ts/track1 RTSP/1.0 CSeq: 4 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22) Transport: RTP/AVP;unicast;client_port=56136-56137 複製程式碼
handleCmd_SETUP,主線如下:
- 根據URL中檔名bipbop-gear1-all.ts(如有子目錄,則為完整,如/dir/bipbop-gear1-all.ts)查詢ServerMediaSession。如找到,增加引用計數,如未找到,handleCmd_notFound()也就是回覆"404 未找到流"錯誤後退出。
- 如指定流已存在,則先停止
- 根據Transport頭內容,確定串流模式及其他引數。如RTP/AVP/TCP對應TCP模式,而RAW/RAW/UDP或/MP2T/H2221/UDP對應UDP模式。本例中使用TCP模式進行傳輸,且可進一步確認RTP埠為56136,RTCP埠為56137
- 檢查是否帶Range或x-playNoew頭,以判斷是否要在SETUP後立即PLAY
- getStreamParameters
主要是生成serverRTPPort、serverRTCPPort及如下重要元件:
- createNewStreamSource
- createNewRTPSink 後兩者要著重注意,將是下一篇的入口之一。 SETUP操作最大的變化是建立了ServerMediaSession,在後續PLAY/PAUSE/TEARDOWN等操作中均會使用到。
回覆示例:
RTSP/1.0 200 OK CSeq: 4 Date: Fri, May 26 2017 13:06:44 GMT Transport:RTP/AVP;unicast;destination=192.168.56.1;source=192.168.56.1; client_port=55436-55437;server_port=6970-6971 Session: 050BAAB9;timeout=65 複製程式碼
-
PLAY 請求示例:
PLAY rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0 CSeq: 5 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22) Session: 050BAAB9 Range: npt=0.000- 複製程式碼
handleCmd_withinSession -> handleCmd_PLAY()思路如下:
- 檢測是否存在Scale頭,如存在,更新為指定值,否則為預設值1.0
- 測試Scale值是否可行
- 檢測是否存在Range頭,根據值情況設定duration
- 播放前設定為指定Scale、Range
- 開始Streaming,預設操作為呼叫其startPlaying()。需注意引數handleAlternativeRequestByte,這說明又開啟了一項新服務,至於PLAY中發生的具體操作,將單獨成篇說明。
fStreamStates[i].subsession->startStream(fOurSessionId, fStreamStates[i].streamToken, (TaskFunc*)noteClientLiveness, this, rtpSeqNum, rtpTimestamp, RTSPServer::RTSPClientConnection::handleAlternativeRequestByte, ourClientConnection); 複製程式碼
回覆示例:
RTSP/1.0 200 OK CSeq: 5 Date: Fri, May 26 2017 13:06:44 GMT Range: npt=0.000- Session: 050BAAB9 RTP-Info: url=rtsp://192.168.56.1/bipbop-gear1-all.ts/track1;seq=39939;rtptime=3398276543 複製程式碼
-
GET_PARAMETER 請求示例:
GET_PARAMETER rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0 CSeq: 6 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22) Session: 050BAAB9 0.000- 複製程式碼
handleCmd_GET_PARAMETER直接生成回覆內容,無其他操作。
回覆示例:
RTSP/1.0 200 OK CSeq: 6 Date: Fri, May 26 2017 13:06:44 GMT Session: 050BAAB9 Content-Length: 10 2017.04.10 複製程式碼
-
TEARDOWN 請求示例:
TEARDOWN rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0 CSeq: 7 User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22) Session: 050BAAB9 複製程式碼
handleCmd_TEARDOWN中釋放Socket、刪除流等資源,並回復"200"結果。
回覆示例:
RTSP/1.0 200 OK CSeq: 7 Date: Fri, May 26 2017 13:06:44 GMT 複製程式碼
- SET_PARAMETER handleCmd_SET_PARAMETER直接生成回覆內容,無其他操作。
-
PAUSE handleCmd_PAUSE最終呼叫了RTPSink/UDPSink上的StopPlaying()介面。
4. 總結
綜上所述,客戶端與伺服器進行連線過程實際上就是為RTSP會話互動過程,而其中會進一步產生連鎖反應的步驟主要有:
-
SETUP
- createNewStreamSource
- createNewRTPSink
-
PLAY
- handleAlternativeRequestByte
篇幅所限,將根據這些線索展開下一篇。