以testRTSPClient.cpp测试程序来对Live555 RTSP播放进行一个简单的分析。同时对Live555几大模块的功能及使用进行简单描述。
因为我对Live555使用的比较多的是在客户端播放场景下,所以可能有些不足或者错误,请指正。
上一章节描述了Live555基础模块,具备这些知识后,我们进入主题,来分析RTSP播放流程,其中最主要的流程在RTSPClient及MediaSession中。
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命令。
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载流
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命令发送前,需要发一个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相关代码。