上篇文章 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