上篇文章 live555学习笔记【3】---RTSP服务器(一) 上篇文已经将整个RTSP服务器的处理流程分析过了,接下来我们再细看RTSP服务器对客户端报文的处理。本篇博客需要深入分析服务器对客户端报文的处理,所以要求你对RTSP、SDP协议有一定的了解.
我们先来分析一下客户端信息处理函数handleRequestBytes(中的)RTSP报文解析函数 parseRTSPRequestString():里面的东西很少,就不上代码了,直接讲一下函数到底做了什么。其实就是按照RTSP协议,解析各个字段的内容,并以传出参数的形式传出来。
我们还是来看看,服务器是怎么处理各个RTSP命令的吧!
1、OPTION命令:
void RTSPServer::RTSPClientConnection::handleCmd_OPTIONS() { //其实就是按照RTSP协议组装一个响应包,其中allowedCommandNames()的值也是写死的,为:"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER" 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()); }
2、SET_PARAMETER及GET_PARAMETER命令
这两个命令服务器什么都不做,直接返回200 OK,如果自己的服务器对这两个命令有要求,就需要自己继承服务器类后,自己实现了。
3、DESCRIBE命令
RTSP服务器收到客户端的DESCRIBE请求后,根据请求URL(rtsp://192.168.0.1/1.mpg),找到对应的流媒体资源,返回响应消息。live555中的ServerMediaSession类用来处理会话中描述,它包含多个(音频或视频)的子会话描述(ServerMediaSubsession)。 RTSP服务器收到客户端的连接请求,建立了RTSPClientSession类,处理单独的客户会话。在建立RTSPClientSession的过程中,将新建立的socket句柄(clientSocket)和RTSP请求处理函数句柄RTSPClientSession::incomingRequestHandler传给任务调度器,由任务调度器对两者进行一对一关联。
收到客户端请求后,如果发现请求是DESCRIBE则进入handleCmd_DESCRIBE函数。根据客户端请求URL的后缀(如1.mpg),调用成员函数DynamicRTSPServer::lookupServerMediaSession查找对应的流媒体信息ServerMediaSession。如果ServerMediaSession不存在,但是本地存在1.mpg文件,则创建一个新的ServerMediaSession。在创建ServerMediaSession过程中,根据文件后缀.mpg,创建媒体MPEG-1or2的解复用器(MPEG1or2FileServerDemux)。再由MPEG1or2FileServerDemux创建一个子会话描述MPEG1or2DemuxedServerMediaSubsession。最后由ServerMediaSession完成组装响应消息中的SDP信息,然后将响应消息发给客户端,完成一次消息交互。
下面我们具体来看看服务器是如何相应DESCRIBE命令的,我会加些注释,帮助你理解。
void RTSPServer::RTSPClientConnection ::handleCmd_DESCRIBE(char const* urlPreSuffix, char const* urlSuffix, char const* fullRequestStr) { ServerMediaSession* session = NULL; char* sdpDescription = NULL; char* rtspURL = NULL; do { char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX]; // enough space for urlPreSuffix/urlSuffix'\0' urlTotalSuffix[0] = '\0'; if (urlPreSuffix[0] != '\0') { strcat(urlTotalSuffix, urlPreSuffix); strcat(urlTotalSuffix, "/"); } //获取客户端请求的媒体的相对路径 strcat(urlTotalSuffix, urlSuffix); //客户端校验,这里均返回TRUE if (!authenticationOK("DESCRIBE", urlTotalSuffix, fullRequestStr)) break; // We should really check that the request contains an "Accept:" ##### // for "application/sdp", because that's what we're sending back ##### // Begin by looking up the "ServerMediaSession" object for the specified "urlTotalSuffix": //根据路径找到会话描述 session = fOurServer.lookupServerMediaSession(urlTotalSuffix); if (session == NULL) { handleCmd_notFound(); break; } // Increment the "ServerMediaSession" object's reference count, in case someone removes it // while we're using it: //该会话描述自增1,防止会话内容被误删除 session->incrementReferenceCount(); // Then, assemble a SDP description for this session: //根据媒体信息组装SDP包 sdpDescription = session->generateSDPDescription(); if (sdpDescription == NULL) { // This usually means that a file name that was specified for a // "ServerMediaSubsession" does not exist. setRTSPResponse("404 File Not Found, Or In Incorrect Format"); break; } unsigned sdpDescriptionSize = strlen(sdpDescription); // Also, generate our RTSP URL, for the "Content-Base:" header // (which is necessary to ensure that the correct URL gets used in subsequent "SETUP" requests). //返回服务器自己的rtsp地址,防止客户端误发 rtspURL = fOurRTSPServer.rtspURL(session, fClientInputSocket); snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" "%s" "Content-Base: %s/\r\n" "Content-Type: application/sdp\r\n" "Content-Length: %d\r\n\r\n" "%s", fCurrentCSeq, dateHeader(), rtspURL, sdpDescriptionSize, sdpDescription); } while (0); //根据使用计数的方式,判断当前会话描述是否需要删除 if (session != NULL) { // Decrement its reference count, now that we're done using it: session->decrementReferenceCount(); if (session->referenceCount() == 0 && session->deleteWhenUnreferenced()) { fOurServer.removeServerMediaSession(session); } } delete[] sdpDescription; delete[] rtspURL; }
4、SETUP命令
类成员函数handleCmd_SETUP()处理客户端的SETUP请求。调用parseTransportHeader()对SETUP请求的传输头解析,调用子会话(这里具体实现类为OnDemandServerMediaSubsession)的getStreamParameters()函数获取流媒体发送传输参数。将这些参数组装成响应消息,返回给客户端。
获取发送传输参数的过程:调用子会话(具体实现类MPEG1or2DemuxedServerMediaSubsession)的createNewStreamSource(...)创建MPEG1or2VideoStreamFramer,选择发送传输参数,并调用子会话的createNewRTPSink(...)创建MPEG1or2VideoRTPSink。同时将这些信息保存在StreamState类对象中,用于记录流的状态。
客户端发送两个SETUP请求,分别用于建立音频和视频的RTP接收。
SETUP命令的响应相对比较复杂,这里我先整理说明一下,之后再去分析源码,可能效果会更好。首先是判断客户端请求的媒体流是否存在->判断媒体流的子会话是否存在->解析报文头->组装报文到发送缓冲区.
void RTSPServer::RTSPClientSession ::handleCmd_SETUP(RTSPServer::RTSPClientConnection* ourClientConnection, char const* urlPreSuffix, char const* urlSuffix, char const* fullRequestStr) { // Normally, "urlPreSuffix" should be the session (stream) name, and "urlSuffix" should be the subsession (track) name. // However (being "liberal in what we accept"), we also handle 'aggregate' SETUP requests (i.e., without a track name), // in the special case where we have only a single track. I.e., in this case, we also handle: // "urlPreSuffix" is empty and "urlSuffix" is the session (stream) name, or // "urlPreSuffix" concatenated with "urlSuffix" (with "/" inbetween) is the session (stream) name. char const* streamName = urlPreSuffix; // in the normal case char const* trackId = urlSuffix; // in the normal case char* concatenatedStreamName = NULL; // in the normal case //这里先判断客户端请求的媒体流是否存在 do { // First, make sure the specified stream name exists: ServerMediaSession* sms = fOurServer.lookupServerMediaSession(streamName, fOurServerMediaSession == NULL); if (sms == NULL) { // Check for the special case (noted above), before we give up: if (urlPreSuffix[0] == '\0') { streamName = urlSuffix; } else { concatenatedStreamName = new char[strlen(urlPreSuffix) + strlen(urlSuffix) + 2]; // allow for the "/" and the trailing '\0' sprintf(concatenatedStreamName, "%s/%s", urlPreSuffix, urlSuffix); streamName = concatenatedStreamName; } trackId = NULL; // Check again: sms = fOurServer.lookupServerMediaSession(streamName, fOurServerMediaSession == NULL); } if (sms == NULL) { if (fOurServerMediaSession == NULL) { // The client asked for a stream that doesn't exist (and this session descriptor has not been used before): ourClientConnection->handleCmd_notFound(); } else { // The client asked for a stream that doesn't exist, but using a stream id for a stream that does exist. Bad request: ourClientConnection->handleCmd_bad(); } break; } else { if (fOurServerMediaSession == NULL) { // We're accessing the "ServerMediaSession" for the first time. fOurServerMediaSession = sms; fOurServerMediaSession->incrementReferenceCount(); } else if (sms != fOurServerMediaSession) { // The client asked for a stream that's different from the one originally requested for this stream id. Bad request: ourClientConnection->handleCmd_bad(); break; } } //第一次为该媒体流创建会话,需要再创建几个他的子会话,例如一段264视频,包含视频及音频两个子会话 if (fStreamStates == NULL) { // This is the first "SETUP" for this session. Set up our array of states for all of this session's subsessions (tracks): fNumStreamStates = fOurServerMediaSession->numSubsessions(); fStreamStates = new struct streamState[fNumStreamStates]; ServerMediaSubsessionIterator iter(*fOurServerMediaSession); ServerMediaSubsession* subsession; for (unsigned i = 0; i < fNumStreamStates; ++i) { subsession = iter.next(); fStreamStates[i].subsession = subsession; fStreamStates[i].tcpSocketNum = -1; // for now; may get set for RTP-over-TCP streaming fStreamStates[i].streamToken = NULL; // for now; it may be changed by the "getStreamParameters()" call that comes later } } //确认客户端请求的子会话是否存在 // Look up information for the specified subsession (track): ServerMediaSubsession* subsession = NULL; unsigned trackNum; if (trackId != NULL && trackId[0] != '\0') { // normal case for (trackNum = 0; trackNum < fNumStreamStates; ++trackNum) { subsession = fStreamStates[trackNum].subsession; if (subsession != NULL && strcmp(trackId, subsession->trackId()) == 0) break; } if (trackNum >= fNumStreamStates) { // The specified track id doesn't exist, so this request fails: ourClientConnection->handleCmd_notFound(); break; } } else { // Weird case: there was no track id in the URL. // This works only if we have only one subsession: if (fNumStreamStates != 1 || fStreamStates[0].subsession == NULL) { ourClientConnection->handleCmd_bad(); break; } trackNum = 0; subsession = fStreamStates[trackNum].subsession; } // ASSERT: subsession != NULL void*& token = fStreamStates[trackNum].streamToken; // alias if (token != NULL) { // We already handled a "SETUP" for this track (to the same client), // so stop any existing streaming of it, before we set it up again: subsession->pauseStream(fOurSessionId, token); fOurRTSPServer.unnoteTCPStreamingOnSocket(fStreamStates[trackNum].tcpSocketNum, this, trackNum); subsession->deleteStream(fOurSessionId, token); } // Look for a "Transport:" header in the request string, to extract client parameters: StreamingMode streamingMode; char* streamingModeString = NULL; // set when RAW_UDP streaming is specified char* clientsDestinationAddressStr; u_int8_t clientsDestinationTTL; portNumBits clientRTPPortNum, clientRTCPPortNum; unsigned char rtpChannelId, rtcpChannelId; //下面就是解析SETUP请求报文,主要是其中的网络传输类型字段,起止时间、端口号等 parseTransportHeader(fullRequestStr, streamingMode, streamingModeString, clientsDestinationAddressStr, clientsDestinationTTL, clientRTPPortNum, clientRTCPPortNum, rtpChannelId, rtcpChannelId); if ((streamingMode == RTP_TCP && rtpChannelId == 0xFF) || (streamingMode != RTP_TCP && ourClientConnection->fClientOutputSocket != ourClientConnection->fClientInputSocket)) { // An anomolous situation, caused by a buggy client. Either: // 1/ TCP streaming was requested, but with no "interleaving=" fields. (QuickTime Player sometimes does this.), or // 2/ TCP streaming was not requested, but we're doing RTSP-over-HTTP tunneling (which implies TCP streaming). // In either case, we assume TCP streaming, and set the RTP and RTCP channel ids to proper values: streamingMode = RTP_TCP; rtpChannelId = fTCPStreamIdCount; rtcpChannelId = fTCPStreamIdCount+1; } if (streamingMode == RTP_TCP) fTCPStreamIdCount += 2; Port clientRTPPort(clientRTPPortNum); Port clientRTCPPort(clientRTCPPortNum); // Next, check whether a "Range:" or "x-playNow:" header is present in the request. // This isn't legal, but some clients do this to combine "SETUP" and "PLAY": double rangeStart = 0.0, rangeEnd = 0.0; char* absStart = NULL; char* absEnd = NULL; Boolean startTimeIsNow; if (parseRangeHeader(fullRequestStr, rangeStart, rangeEnd, absStart, absEnd, startTimeIsNow)) { delete[] absStart; delete[] absEnd; fStreamAfterSETUP = True; } else if (parsePlayNowHeader(fullRequestStr)) { fStreamAfterSETUP = True; } else { fStreamAfterSETUP = False; } // Then, get server parameters from the 'subsession': if (streamingMode == RTP_TCP) { // Note that we'll be streaming over the RTSP TCP connection: fStreamStates[trackNum].tcpSocketNum = ourClientConnection->fClientOutputSocket; fOurRTSPServer.noteTCPStreamingOnSocket(fStreamStates[trackNum].tcpSocketNum, this, trackNum); } netAddressBits destinationAddress = 0; u_int8_t destinationTTL = 255; #ifdef RTSP_ALLOW_CLIENT_DESTINATION_SETTING if (clientsDestinationAddressStr != NULL) { // Use the client-provided "destination" address. // Note: This potentially allows the server to be used in denial-of-service // attacks, so don't enable this code unless you're sure that clients are // trusted. destinationAddress = our_inet_addr(clientsDestinationAddressStr); } // Also use the client-provided TTL. destinationTTL = clientsDestinationTTL; #endif delete[] clientsDestinationAddressStr; Port serverRTPPort(0); Port serverRTCPPort(0); // Make sure that we transmit on the same interface that's used by the client (in case we're a multi-homed server): struct sockaddr_in sourceAddr; SOCKLEN_T namelen = sizeof sourceAddr; getsockname(ourClientConnection->fClientInputSocket, (struct sockaddr*)&sourceAddr, &namelen); netAddressBits origSendingInterfaceAddr = SendingInterfaceAddr; netAddressBits origReceivingInterfaceAddr = ReceivingInterfaceAddr; // NOTE: The following might not work properly, so we ifdef it out for now: #ifdef HACK_FOR_MULTIHOMED_SERVERS ReceivingInterfaceAddr = SendingInterfaceAddr = sourceAddr.sin_addr.s_addr; #endif subsession->getStreamParameters(fOurSessionId, ourClientConnection->fClientAddr.sin_addr.s_addr, clientRTPPort, clientRTCPPort, fStreamStates[trackNum].tcpSocketNum, rtpChannelId, rtcpChannelId, destinationAddress, destinationTTL, fIsMulticast, serverRTPPort, serverRTCPPort, fStreamStates[trackNum].streamToken); SendingInterfaceAddr = origSendingInterfaceAddr; ReceivingInterfaceAddr = origReceivingInterfaceAddr; AddressString destAddrStr(destinationAddress); AddressString sourceAddrStr(sourceAddr); char timeoutParameterString[100]; if (fOurRTSPServer.fReclamationSeconds > 0) { sprintf(timeoutParameterString, ";timeout=%u", fOurRTSPServer.fReclamationSeconds); } else { timeoutParameterString[0] = '\0'; } if (fIsMulticast) { switch (streamingMode) { case RTP_UDP: { //组装响应报文,并放入响应缓冲区中 snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, "RTSP/1.0 200 OK\r\n" "CSeq: %s\r\n" "%s" "Transport: RTP/AVP;multicast;destination=%s;source=%s;port=%d-%d;ttl=%d\r\n" "Session: %08X%s\r\n\r\n", ourClientConnection->fCurrentCSeq, dateHeader(), destAddrStr.val(), sourceAddrStr.val(), ntohs(serverRTPPort.num()), ntohs(serverRTCPPort.num()), destinationTTL, fOurSessionId, timeoutParameterString); break; } case RTP_TCP: { // multicast streams can't be sent via TCP ourClientConnection->handleCmd_unsupportedTransport(); break; } case RAW_UDP: { snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, "RTSP/1.0 200 OK\r\n" "CSeq: %s\r\n" "%s" "Transport: %s;multicast;destination=%s;source=%s;port=%d;ttl=%d\r\n" "Session: %08X%s\r\n\r\n", ourClientConnection->fCurrentCSeq, dateHeader(), streamingModeString, destAddrStr.val(), sourceAddrStr.val(), ntohs(serverRTPPort.num()), destinationTTL, fOurSessionId, timeoutParameterString); break; } } } else { switch (streamingMode) { case RTP_UDP: { snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, "RTSP/1.0 200 OK\r\n" "CSeq: %s\r\n" "%s" "Transport: RTP/AVP;unicast;destination=%s;source=%s;client_port=%d-%d;server_port=%d-%d\r\n" "Session: %08X%s\r\n\r\n", ourClientConnection->fCurrentCSeq, dateHeader(), destAddrStr.val(), sourceAddrStr.val(), ntohs(clientRTPPort.num()), ntohs(clientRTCPPort.num()), ntohs(serverRTPPort.num()), ntohs(serverRTCPPort.num()), fOurSessionId, timeoutParameterString); break; } case RTP_TCP: { if (!fOurRTSPServer.fAllowStreamingRTPOverTCP) { ourClientConnection->handleCmd_unsupportedTransport(); } else { snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, "RTSP/1.0 200 OK\r\n" "CSeq: %s\r\n" "%s" "Transport: RTP/AVP/TCP;unicast;destination=%s;source=%s;interleaved=%d-%d\r\n" "Session: %08X%s\r\n\r\n", ourClientConnection->fCurrentCSeq, dateHeader(), destAddrStr.val(), sourceAddrStr.val(), rtpChannelId, rtcpChannelId, fOurSessionId, timeoutParameterString); } break; } case RAW_UDP: { snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, "RTSP/1.0 200 OK\r\n" "CSeq: %s\r\n" "%s" "Transport: %s;unicast;destination=%s;source=%s;client_port=%d;server_port=%d\r\n" "Session: %08X%s\r\n\r\n", ourClientConnection->fCurrentCSeq, dateHeader(), streamingModeString, destAddrStr.val(), sourceAddrStr.val(), ntohs(clientRTPPort.num()), ntohs(serverRTPPort.num()), fOurSessionId, timeoutParameterString); break; } } } delete[] streamingModeString; } while (0); delete[] concatenatedStreamName; }
5、PLAY命令
PLAY命令的响应很简单,但是服务器收到PLAY请求后做的处理就很复杂了,这里,我先讲一下PLAY命令的流程吧!收到PLAY命令->解析报文,判断倍速播放(实际上只支持单倍速)->解析报文,判断起止时间->根据上面获取到的参数,设置各子会话的参数->设置RTP/RTCP包的处理函数->组装报文,放入响应缓冲区。
类成员函数handleCmd_PLAY()处理客户端的播放请求。首先调用子会话的startStream(),内部调用MediaSink::startPlaying(...),然后是MultiFramedRTPSink::continuePlaying(),接着调用MultiFramedRTPSink::buildAndSendPacket(...)。buildAndSendPacke内部先设置RTP包头,内部再调用MultiFramedRTPSink::packFrame()填充编码帧数据。
packFrame内部通过FramedSource::getNextFrame(), 接着MPEGVideoStreamFramer::doGetNextFrame(),再接着经过MPEGVideoStreamFramer::continueReadProcessing(), FramedSource::afterGetting(...), MultiFramedRTPSink::afterGettingFrame(...), MultiFramedRTPSink::afterGettingFrame1(...)等一系列繁琐调用,最后到了MultiFramedRTPSink::sendPacketIfNecessary(), 这里才真正发送RTP数据包。然后是计算下一个数据包发送时间,把MultiFramedRTPSink::sendNext(...)函数句柄传给任务调度器,作为一个延时事件调度。在主循环中,当MultiFramedRTPSink::sendNext()被调度时,又开始调用MultiFramedRTPSink::buildAndSendPacket(...)开始新的发送数据过程,这样客户端可以源源不断的收到服务器传来的RTP包了。
6、TEARDOWN命令
直接释放各类资源,然后回复200 OK