以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相關代碼。