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相关代码。

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