Live555原始碼解析(4) - 魚兒上鉤來

weixin_34148340發表於2017-05-27

上一篇Live555原始碼解析(3) - 服務開啟,願者上鉤中我們講到RTSPServer建立後,帶來了兩項重要支援(旁註類比):

  • incomingConnectionHandler服務 - 掛著魚餌的釣鉤
  • 雜湊表
    • ServerMediaSessions - 釣竿與手之間所有互動
    • ClientConnections - 釣線
    • ClientSessions - 釣鉤與魚之間所有互動

整理一下,其實伺服器已經擺好姿勢,嚴陣以待第一個吃魚餌的魚上鉤了,那麼接下來,我們就看魚兒是怎麼一步一步上鉤來。

1. 魚

釣魚要有魚,服務要有被服務者,Live555媒體伺服器的服務物件就是支援RTSP/RTP協議的客戶端。從官網對客戶端介紹中我們可以看到,目前支援如下主流客戶端:

要釣就釣大魚,本篇就採用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

篇幅所限,將根據這些線索展開下一篇。

相關文章