Live555 RTSP播放分析(二)--RTSPClient及MediaSession

以testRTSPClient.cpp測試程序來對Live555 RTSP播放進行一個簡單的分析。同時對Live555幾大模塊的功能及使用進行簡單描述。
因爲我對Live555使用的比較多的是在客戶端播放場景下,所以可能有些不足或者錯誤,請指正。

上一章節描述了Live555基礎模塊,具備這些知識後,我們進入主題,來分析RTSP播放流程,其中最主要的流程在RTSPClient及MediaSession中。

testRTSPClient

testRTSPClient main函數其實就做了幾個事情:
1、創建scheduler 及env;
2、打開播放地址;
3、讓程序進入消息循環跑起來。env->taskScheduler().doEventLoop(&eventLoopWatchVariable);

int main(int argc, char** argv) {
  // Begin by setting up our usage environment:
  TaskScheduler* scheduler = BasicTaskScheduler::createNew();
  UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);

  // We need at least one "rtsp://" URL argument:
  if (argc < 2) {
    usage(*env, argv[0]);
    return 1;
  }
  openURL(*env, argv[0], argv[1]);
  if(argc >= 3 && strstr(argv[2],"tcp")){
      REQUEST_STREAMING_OVER_TCP = true;
  }


  env->taskScheduler().doEventLoop(&eventLoopWatchVariable);
  return 0;

openURL中創建了ourRTSPClient,其是RTSPClient的子類。然後調用rtspClient->sendDescribeCommand(continueAfterDESCRIBE);發出DESCRIBE。
continueAfterDESCRIBE爲回調函數。

DESCRIBE

DESCRIBE比較簡單,就是發出DESCRIBE命令。

unsigned RTSPClient::sendDescribeCommand(responseHandler* responseHandler, Authenticator* authenticator) {
  if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
  return sendRequest(new RequestRecord(++fCSeq, "DESCRIBE", responseHandler));
}

簡單分析下sendRequest:
首先建立TCP連接int connectResult = openConnection();

int RTSPClient::openConnection() {
  do {
   ...省略
   //1.解析是否需要賬號密碼,有些RTSP連接是攜帶賬號密碼的,這個在視頻監控領域比較常見:
    if (!parseRTSPURL(envir(), fBaseURL, username, password, destAddress, urlPortNum, &urlSuffix)) break;
    portNumBits destPortNum = fTunnelOverHTTPPortNum == 0 ? urlPortNum : fTunnelOverHTTPPortNum;
    if (username != NULL || password != NULL) {
      fCurrentAuthenticator.setUsernameAndPassword(username, password);
      delete[] username;
      delete[] password;
    }
    
     //2.建立TCP Socket,連接服務器:
    fInputSocketNum = fOutputSocketNum = setupStreamSocket(envir(), Port(0), destAddress.getFamily());
    if (fInputSocketNum < 0) break;
    ignoreSigPipeOnSocket(fInputSocketNum); // so that servers on the same host that get killed don't also kill us      
    // Connect to the remote endpoint:
    fServerAddress = destAddress;
    int connectResult = connectToServer(fInputSocketNum, destPortNum);
    if (connectResult < 0) break;
    else if (connectResult > 0) {
      // 3.連接成功,在taskScheduler輪訓IO,socket讀到數據的回調函數爲incomingDataHandler
      envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION,
						    (TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
  ......省略

然後是RTSP命令封裝發出。
socket回調函數incomingDataHandler讀取RTSP命令響應。

void RTSPClient::incomingDataHandler(void* instance, int /*mask*/) {
  RTSPClient* client = (RTSPClient*)instance;
  client->incomingDataHandler1();
}

void RTSPClient::incomingDataHandler1() {
  NetAddress dummy; // 'from' address - not used

  int bytesRead = readSocket(envir(), fInputSocketNum, (unsigned char*)&fResponseBuffer[fResponseBytesAlreadySeen], fResponseBufferBytesLeft, dummy);
  handleResponseBytes(bytesRead);
}

handleResponseBytes處理RTSP命令的響應,解析各種RTSP響應頭字段,如"RTP-Info:""Range:"等等,對於SETUP/PLAY等命令,會有一些不同的處理。然後根據responseCode,看是否正常,常見的錯誤像403,401。302則需要重定向,200則正常。
最後調用(*foundRequest->handler())(this, resultCode, resultString);調用回調函數

回到DESCRIBE命令的話,並沒什麼特許處理,回調函數是continueAfterDESCRIBE,主要工作爲根據SDP信息創建MediaSession,並setupNextSubsession

   ......省略
    // Create a media session object from this SDP description:
    scs.session = MediaSession::createNew(env, sdpDescription);
    delete[] sdpDescription; // because we don't need it anymore
    if (scs.session == NULL) {
      break;
    } else if (!scs.session->hasSubsessions()) {
      env << *rtspClient << "This session has no media subsessions (i.e., no \"m=\" lines)\n";
      break;
    }

    scs.iter = new MediaSubsessionIterator(*scs.session);
    setupNextSubsession(rtspClient);
    return;
  } while (0);

這裏要明確的是,MediaSession可以理解爲一次播放會話,MediaSubsession是每一個播放鏈接會話。MediaSubsession根據媒體描述字段“m=”來創建,RTSP播放是存在多個媒體的,例如音頻視頻分開的場景。但一般TS的播放場景中,都是隻有一個,例如下面的SDP只有一個MediaSubsession

v=0
o=- 0 0 IN IP4 61.149.64.212
s=ZMSS RTSP Server
c=IN IP4 239.2.1.232/16 
b=AS:2500 
t=0 0
a=control:*
a=range:clock=20180503T064832.00Z-20180510T064832.00Z
m=video 8000 RTP/AVP 33
a=rtpmap:33 MP2T/90000
a=control:trackID=2
a=3GPP-Adaptation-Support:5

回到MediaSession的createNew函數,主要是initializeWithSDP函數,其中會解析出封裝協議及編碼方式,後面需要根據此來創建Source。每一個"m="字段會創建一個MediaSubsession,最後所有MediaSubsession會存放到鏈表MediaSubsessionIterator裏。
另外就是根據SDP協議,解析其他信息。SDP具體可參考RTSP簡介

創建完MediaSession後,則setup每一個MediaSubsession。

void setupNextSubsession(RTSPClient* rtspClient) {
  UsageEnvironment& env = rtspClient->envir(); // alias
  StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias
  
  scs.subsession = scs.iter->next();
  if (scs.subsession != NULL) {
    if (!scs.subsession->initiate()) {
    	//init失敗,setup下個鏈接
      setupNextSubsession(rtspClient); // give up on this subsession; go to the next one
    } else {
     //init成功,發送SETUP命令,
      rtspClient->sendSetupCommand(*scs.subsession, continueAfterSETUP, False, REQUEST_STREAMING_OVER_TCP);
    }
    return;
  }
  //全部鏈接都建立好了,發送PLAY
  scs.duration = scs.session->playEndTime() - scs.session->playStartTime();
  rtspClient->sendPlayCommand(*scs.session, continueAfterPLAY);
}

首先是初始化scs.subsession->initiate(),會使用SDP信息中”C=”後面的IP地址信息,建立fRTPSocket。例如上面的SDP例子c=IN IP4 239.2.1.232/16 ,IP地址就是239.2.1.232

initiate():

	if (isSSM()) {
	  fRTPSocket = new Groupsock(env(), tempAddr, fSourceFilterAddr, Port(0));
	} else {
	  fRTPSocket = new Groupsock(env(), tempAddr, Port(0), 255);
	}

然後根據SDP解出來的封裝協議及編碼方式,創建Source

initiate():

    // Create "fRTPSource" and "fReadSource":
    if (!createSourceObjects(useSpecialRTPoffset)) break;

載流協議方式有兩種,UDP裸流,還是基於RTP。編碼方式則有很多。
下面這個例子:協議基於RTP,編碼方式爲MP2T

m=video 0 RTP/AVP 33
b=RR:0
a=rtpmap:33 MP2T/90000

那麼看一下對應的Source創建:
創建SimpleRTPSource,Filter爲MPEG2TransportStreamFramer,如下

createSourceObjects:

else if (strcmp(fCodecName, "MP2T") == 0) { // MPEG-2 Transport Stream
	fRTPSource = SimpleRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
						fRTPTimestampFrequency, "video/MP2T",
						0, False);
	fReadSource = MPEG2TransportStreamFramer::createNew(env(), fRTPSource);
	// this sets "durationInMicroseconds" correctly, based on the PCR values
}

基本上scs.subsession->initiate()就完成了,主要就是根據SDP創建了Source。然後發送SETUP命令,回調函數continueAfterSETUP

rtspClient->sendSetupCommand(*scs.subsession, continueAfterSETUP, False, REQUEST_STREAMING_OVER_TCP);//TCP載流

SETUP

STEUP命令需要根據SDP中的協議來確定Transport: 字段,爲傳輸模式
如果是UDP裸流,前綴爲RAW/RAW/UDP,RTP則爲RTP/AVP

RTP/AVP默認使用UDP傳輸RTP包,RTP/AVP/TCP表示通過TCP傳輸RTP包。
unicast表示單一傳播。
client_port值中-前的表示客戶端的接收RTP包的端口,-後的表示客戶端的接收RTCP包的端口。
如果採用TCP方式傳送,因爲傳送的RTP,RTCP包都在同一個鏈路上,需要區分,所以有了interleaved,0表示是RTP的通道,1表示是RTCP的通道,interleaved值有兩個:0和1,0表示RTP包,1表示RTCP包,接收端根據interleaved的值來區別是哪種數據包。數據包頭部第二個字節位置就是interleaved。

一些例子:

Transport: RTP/AVP/TCP;unicast;interleaved=0-1	//請求TCP方式傳輸RTP數據包
Transport: RTP/AVP/UDP;unicast;client_port=36900-36901	//請求UDP方式傳輸RTP數據包

代碼如下:

setRequestFields:

{
   ......省略
    
    char const* transportFmt;
    if (strcmp(subsession.protocolName(), "UDP") == 0) {
      suffix = "";
      transportFmt = "Transport: RAW/RAW/UDP%s%s%s=%d-%d\r\n";
    } else {
      transportFmt = "Transport: RTP/AVP%s%s%s=%d-%d\r\n";
      if(strcmp(subsession.codecName(), "MP2T") == 0){
          transportFmt = "Transport: MP2T/RTP%s%s%s=%d-%d\r\n";//mark by wusc just for iptv
      }
    }
    
 ......省略
    if (streamUsingTCP) { // streaming over the RTSP connection
      transportTypeStr = "/TCP;unicast";
      portTypeStr = ";interleaved";
      rtpNumber = fTCPStreamIdCount++;
      rtcpNumber = fTCPStreamIdCount++;
    } else { // normal RTP streaming
      NetAddress none;
      NetAddress connectionAddress = subsession.connectionEndpointAddress();
      Boolean requestMulticastStreaming
	= IsMulticastAddress(connectionAddress) || (connectionAddress == none && forceMulticastOnUnspecified);
      transportTypeStr = requestMulticastStreaming ? "/UDP;multicast" : "/UDP;unicast";
      portTypeStr = requestMulticastStreaming ? ";port" : ";client_port";
      rtpNumber = subsession.clientPort().port();//mark by wusc get ntohs(port num)
      if (rtpNumber == 0) {
	envir().setResultMsg("Client port number unknown\n");
	delete[] cmdURL;
	return False;
      }
      rtcpNumber = subsession.rtcpIsMuxed() ? rtpNumber : rtpNumber + 1;
    }
......省略
  }

發送SETUP命令後,等待服務器響應並解析。還是在handleResponseBytes中對SETUP響應有專門的處理handleSETUPResponse,主要工作如下:
1)parseTransportParams解析服務器響應的Transport參數。然後設置給subsession

TCP:需要解析出interleaved的ID號
RTSP/1.0 200 OK
Server: UServer 0.9.7_rc1
Cseq: 3
Session: 6310936469860791894     //服務器迴應的會話標識符
Cache-Control: no-cache
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=6B8B4567

UDP:需要解析出source及服務器地址,server_port即服務器RTP/RTCP端口
RTSP/1.0 200 OK
Server: VLC/3.0.5
Date: Thu, 12 Mar 2020 01:21:14 GMT
Transport: RTP/AVP/UDP;unicast;source=2001:db8::d10:9c26:da9f:ea79;client_port=36900-36901;server_port=52326-52327;ssrc=538D7D5A;mode=play
Session: 938886619d22f023;timeout=60
Content-Length: 0
Cache-Control: no-cache
Cseq: 2

2)TCP載流方式,RTP的Socket即爲fInputSocketNum(這個就是RTSP命令收發的socket),設置rtpChannelId

 if (subsession.rtpSource() != NULL) {
	subsession.rtpSource()->setStreamSocket(fInputSocketNum, subsession.rtpChannelId);
	  // So that we continue to receive & handle RTSP commands and responses from the server
	subsession.rtpSource()->enableRTCPReports() = False;
	  // To avoid confusing the server (which won't start handling RTP/RTCP-over-TCP until "PLAY"), don't send RTCP "RR"s yet
      }

3)UDP載流方式,根據服務器的迴應,重新設置RTPSource 的端口及IP

void MediaSubsession::setDestinations(NetAddress defaultDestAddress) {
  // Get the destination address from the connection endpoint name
  // (This will be 0 if it's not known, in which case we use the default)
  NetAddress destAddress = connectionEndpointAddress();
  NetAddress none;
  if (destAddress == none) destAddress = defaultDestAddress;
  NetAddress destAddr; destAddr = destAddress;

  // The destination TTL remains unchanged:
  int destTTL = ~0; // means: don't change

  if (fRTPSocket != NULL) {
    Port destPort=serverPort;
    fRTPSocket->changeDestinationParameters(destAddr, destPort, destTTL);
  }
  if (fRTCPSocket != NULL && !isSSM() && !fMultiplexRTCPWithRTP) {
    // Note: For SSM sessions, the dest address for RTCP was already set.
    Port destPort = Port(serverPort.num()+1);
    fRTCPSocket->changeDestinationParameters(destAddr, destPort, destTTL);
  }
}

處理完響應消息,即調用回調函數continueAfterSETUP,比較簡單,創建Sink,並啓動。然後繼續setupNextSubsession。

continueAfterSETUP:

    scs.subsession->sink->startPlaying(*(scs.subsession->readSource()),
				       subsessionAfterPlaying, scs.subsession);

回到setupNextSubsession中,但所有的連接都setup完了,就發送PLAY命令。

  scs.duration = scs.session->playEndTime() - scs.session->playStartTime();
  rtspClient->sendPlayCommand(*scs.session, continueAfterPLAY);

PLAY

PLAY命令發送前,需要發一個NAT包,其他沒什麼不一樣。

unsigned RTSPClient::sendPlayCommand(MediaSession& session, responseHandler* responseHandler,
                                     double start, double end, float scale,
                                     Authenticator* authenticator) {
  if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
  sendDummyUDPPackets(session); // hack to improve NAT traversal
  return sendRequest(new RequestRecord(++fCSeq, "PLAY", responseHandler, &session, NULL, 0, start, end, scale));
}

PLAY參數中Range:播放時間支持兩種格式,Range: npt=0.0-end或者Range:clock=20100318T021919.35Z-20100318T031919.80Z

方法1 位置描述,相對時間描述——npt(normalplay time)
•beginning 節目起始點
•now 當前播放點
•end 節目結束點
•相對時間 媒體的相對時間
方法2 時間描述,絕對時間描述——clock,ISO8601時間戳標準
•直接用數字形式表示與起始點的時間

發送PLAY命令後,等待服務器響應並解析。還是在handleResponseBytes中對PLAY響應有專門的處理handlePLAYResponse,主要是解析各種響應參數
Scale、Speed、Range、RTP-Info,RTP-Info中可以攜帶RTP包的信息如URL,序號,時間戳,如:

RTP-Info: url=rtsp://61.149.64.132:12370/live/ch11091521323921117877.sdp/trackID=2;seq=0;rtptime=841899578

回調函數continueAfterPLAY也比較簡單,如果有duration,就創建定時任務,在duration+2秒後停止。自行查看代碼即可。

總結

相對來說,代碼還是比較清晰的,但這裏面不涉及RTP包的解析,這部分也是十分重要的。可以參看MultiFramedRTPSource相關代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章