Live555 源代码分析(五)
1 主程序
1.1 基本概念与实体
下图展示主程序中涉及的主要概念与实体。
MediaServer是服务器的抽象。
- 它创建用于接受客户端连接的socket。它还是其他实体的容器。
ClientConnection是与客户端的数据连接的抽象。
- 当客户端连接服务器时,MediaServer创建ClientConnection的实例,保存在成员fClientConnections[]中。
MediaSession是媒体的抽象。
- 当用户请求建立媒体连接时,ClientConnection创建MediaSession实例,保存在MediaServer的成员fServerMediaSessions[]中。这是一个Hash表,key值是媒体的名字。
媒体中可以有多个通道,MediaSubsession是媒体通道的抽象。
- 创建MediaSession时,同时为其中的通道创建MediaSubsession实例,保存在成员fSubsessionHead指向的链表中。
ClientSession是与客户端的对话的抽象,承载在ClientConnection上。
- 当客户端请求开始播放时,创建ClientSession实例,保存在成员fClientSessions[]中。这是一个Hash表,key值是全局唯一的session id。
StreamState是ClientSession用于挂接到MediaSubsession的中介。
- ClientSession要挂接到MediaSession上,获得媒体源。它的成员fStreamStates[]引用MediaSession中的MediaSubsession实例。
- fStreamStates[]是一个数组,这里的trackNum指它的索引。
1.2 Main()
Main()创建任务调度器,创建RTSPServer实例,将它的socket置于调度器的监听下,最后运行调度器,处理socket事件。
-
调用BasicTaskScheduler::createNew()创建任务调度器,这是一个BasicTaskScheduler实例。
-
引用调度器实例,创建BasicUsageEnvironment实例。
-
调用DynamicRTSPServer::createNew(),创建RTSPServer实例。
- 调用GenericMediaServer::setUpOurSocket()创建TCP socket。createNew()的参数指定本地端口号,这里先指定端口号554。但绑定可能失败,如果失败了,main()会再次调用createNew(),用端口号8554再重试一次。
- 创建的socket保存在GenericMediaServer的成员fServerSocket中。
- 调用TaskSheduler::turnOnBackgroundReadHandling()将socket置于监听状态,回调函数为GenericMediaServer::incomingConnectionHandler()。
-
调用RTSPServer::rtspURLPrefix()得到URL信息,其中调用ourIPAddress()得到本地地址。这个信息用于输出给用户看,没其他用处。
-
调用BasicTaskScheduler0::doEventLoop()调度任务。
GenericMediaServer::setUpOurSocket()创建TCP socket。
- 调用setupStreamSocket()创建TCP socket。
- 调用increaseSendBufferTo()重新设置增加缓存大小。
- 调用listen()开始监听。
1.3 GenericMediaServer::incomingConnectionHandler()
当有新的数据连接请求时,GenericMediaServer::incomingConnectionHandler()被调用。其中调用incomingConnectionHandlerOnSocket(),参数是成员fServerSocket。
- 调用accept(),返回新数据连接的socket,同时得到客户端的地址。
- 调用makeSocketNonBlocking()设置socket为非阻塞模式。
- 调用increaseSendBufferTo()增加发送缓存大小。
- 用新连接的socket和客户端地址,调用RTSPServerSupportingHTTPStreaming::createNewClientConnnection()。
在createNewClientConnection()中,
- 创建RTSPClientConnectionSupportingHTTPStreaming实例。
- ClientConnection的成员fOurSocket保存新连接的socket值,成员fClientAddr保存客户端地址。
- 调用TaskScheduler::turnOnBackgroundReadHanding(),将新连接的socket置于监听状态,回调函数为GenericMediaServer::incomingRequestHandler()。
- 调用RTSPClientConnection::resetRequestBuffer()与接收缓存有关的位置指示变量。
1.4 ClientConnection::incomingRequestHandler()
当数据连接有数据到达时,ClientConnection::incomingRequestHandler()被调用。
-
调用readSocket()读取数据,这是RTCP请求字符串。这里使用的接收缓存是成员fRequestBuffer。
-
调用RTSPClientConnection::handleRequestBytes()解析RTCP请求并处理。
- 调用parseRTSPRequestString()解析请求字符串,得到RTCP命令字及参数,包括:
- cmdName是请求名称。
- urlSuffix是文件名
- cseq是请求序列号。回复的字符串应该包括这个序列号,这样请求者能将回应和请求一一对应。
- SessionIdStr是client session的编号。
-
后面根据请求名称,调用不同的函数处理。如handleCmd_OPTIONS()处理OPTIONS请求等。
-
对于SETUP请求,这里会创建ClientSession实例。
-
对于SETUP请求之后的请求,parseRTSPRequestString()应该得到有效的sessionIdStr,调用GenericMediaServer::lookupClientSession()应该得到对应的ClientSession实例。然后调用RTSPClientSession::handleCmd_withinSession(),其中再调用相应的处理函数。
1.4.1.请求OPTIONS
OPTIONS的请求字符串如下:
OPTIONS rtsp://192.168.2.133:8554/jzl.mp3
RTSP/1.0\r\n
CSeq: 2\r\n
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)\r\n\r\n
调用handleCmd_OPTIONS()处理。
- 它的工作只是填充回复字符串,告诉客户端支持那些请求。
RTSP/1.0 200 OK\r\n
CSeq: 2\r\n
Date: Tue, Apr 28 2020 02:37:34 GMT\r\n
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER\r\n\r\n
1.4.2 请求DESCRIBE
DESCRIBE的请求字符串如下:
DESCRIBE rtsp://192.168.2.133:8554/jzl.mp3
RTSP/1.0\r\n
CSeq: 3\r\n
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)\r\n
Accept: application/sdp\r\n\r\n
调用handleCmd_DESCRIBE()处理。
- 调用DynamicRTSPServer::lookupServerMediaSession()查找ServerMediaSession实例,因为这时实例还不存在,所以这里会先创建一个。
- 用这个ServerMediaSeesin实例调用generateSDPDescription()。
- 调用ourIPAddress()得到本地地址。
- 构建合适的SDP前缀字符串,本地地址是它的一个组成部分。
- 遍历ServerMediaSession的成员fSubsessionHead,对其中的子session,调用OnDemandServerMediaSubsession::sdpLines()得到它的sdp字符串。
- 连接sdp前缀字符串和sdp字符串。
- 调用RTSPServer::rtspURL()得到URL信息。这是头部字符串的一部分。
- 组合所有上述信息成为回复字符串。
如下是DESCRIBE请求的回复字符串。
- 第一部分是头部字符串,第二部分是SDP前缀字符串,第三部分是SDP字符串。
- 这里只有一个子session,”a=control:track1”中的track1是它的通道名字。
RTSP/1.0 200 OK\r\n
CSeq: 3\r\n
Date: Tue, Apr 28 2020 06:31:55 GMT\r\n
Content-Base: rtsp://192.168.2.133:8554/jzl.mp3/\r\n
Content-Type: application/sdp\r\n
Content-Length: 391\r\n\r\n
v=0\r\n
o=- 1588055515931667 1 IN IP4 192.168.2.133\r\n
s=MPEG-1 or 2 Audio, streamed by the LIVE555 Media Server\r\n
i=jzl.mp3\r\n
t=0 0\r\n
a=tool:LIVE555 Streaming Media v2019.12.30\r\n
a=type:broadcast\r\n
a=control:*\r\n
a=range:npt=0-232.590\r\n
a=x-qt-text-nam:MPEG-1 or 2 Audio, streamed by the LIVE555 Media Server\r\n
a=x-qt-text-inf:jzl.mp3\r\n
m=audio 0 RTP/AVP 14\r\n
c=IN IP4 0.0.0.0\r\n
b=AS:128\r\n
a=control:track1\r\n
DynamicRTSPServer::lookupServerMediaSessin()根据key值,在GenericMediaServer的成员fSerMediaSessions中查找对应的实例。fServerMediaSessesions是一个Hash表。这里的key值是文件名“jzl.mp3”。
- 调用fopen()打开文件,得到文件指针。
- 调用全局函数createNewSMS()创建新的MediaSession实例。并调用GenericMediaServer::addServerMediaSession()将它加入成员fServerMediaSesion。
在全局函数createSMS()中,
- 从文件名jzl.mp3中,解析出扩展名“.mp3”。根据扩展名做不同处理。 对于扩展名“.mp3”,++ 调用NEW_SMS(),创建ServerMediaSession实例。
- 调用MP3AudioFileServerMediaSubsession::createNew()。它创建ServerMediaSubsession实例,同时把文件名jzl.mp3保存在成员fFileName中。后面创建MP3FileSource实例时将用到它。
- 把这个ServerMediaSubsession加入ServerMediaSession的子session表。ServerMediaSession的成员fSubsesisionHead和fSubsessionTail指向这个表的首部和尾部。
OnDemandServerMediaSubsession::sdpLines()得到子session的sdp字符串。
- 调用createNewStreamSource()创建FramedSource实例,
- 调用createGroupsock()创建Groupsock实例
- 基于FramedSource和Groupsock实例,调用createNewRTPSink()创建RTPSink实例。
- 调用setSDPLinesFromRTPSink()。其中调用RTPSink的接口构建sdp字符串。
- 清理掉以上创建的所有FramedSource、Groupsock、和RTPSink实例。这时只有ServerMediaSession和ServerMediaSubsession的实例保留下来。
1.4.3 请求SETUP
SETUP的请求字符串如下。它请求jzl.mp3文件的通道track1。
SETUP rtsp://192.168.2.133:8554/jzl.mp3/track1 RTSP/1.0\r\n
CSeq: 4\r\n
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)\r\n
Transport: RTP/AVP;unicast;client_port=65512-65513\r\n\r\n
这里先调用createNewClientSessionWithId()创建ClientSession实例,再调用handleCmd_SETUP()处理。注意这里的handleCmd_SETUP()从属于RTSPClientSession(是它的成员函数),而前面的handleCmd_DESCRIBE()从属于RTSPClientConnection。
createNewClientSessionWithId()实现如下:
- 调用our_random32()得到一个随机数。
- 用这个随机数调用RTSPServer::createNewClientSession()。实际上其中创建了一个RTSPClientSession实例。
- ClientConnection的构造函数调用TaskScheduler::resheduleDelayedTask()设置延时任务,回调函数是ClientSession::LivenessTimeoutTask()。这个函数的作用是在这个Client session实例长时间不工作时,删除它。
- 将这个实例加入GenericMediaServer的成员fClientSessions,这是一个ClientSession实例的Hash表。
- our_random32()实际上在一个循环中调用的,用lookupClientSession()查找这个随机数,看是不是已经使用。这样一直找到不重复的随机数为止。
RTSPClientSession::handleCmd_SETUP()的实现如下。
- 请求字符串中的“jzl.mp3/track1”被分解为它的两个参数:参数urlPreSuffix是“jzl.mp3”,参数urlSuffix是“track1”。
- 用“jzl.mp2”调用lookupServerMediaSession(),找到对应的ServerMediaSession实例。这个实例在handleCmd_DESCRIBE()中已经创建好了。
- 调用numSubsessions()得到ServerMediaSession中的子session数量。
- 新建streamState数组fStreamStates[],长度与子Session数量相同。
- 遍历子session链表,依次设置streamStates[]的成员subsession引用ServerMediaSession的成员fSubsessionsHead链表中的元素。
- 同时比较subsession的trackId成员与“track1”是否相同。如果是,则记住它的trackNum。
- 调用parseTransportHeader()解析得到传输头中包括的数据流配置信息。传递给它的是trackNum引用的streamState实例。
- 传输头包括在子字符串“RTP/AVP;unicast;client_port=65512-65513”中。这里指定数据流采用RTP/AVP协议,单播UDP,RTP连接的客户端端口为65512,RTCP连接的客户端端口为65513。
- 流的目标组播地址可以与发起请求的客户端地址不同。如果不同,则传输头中需要包含“destination=xxx”指定这一点。
- 调用parseRangeHeader()解析得到点播范围。如果失败,则再调用parsePlayNowHeader()看能不能找到。这里没有设置点播范围,两个函数都返回失败结果,所以从头开始播到尾。
- 用前面找到的子session,调用OnDemandSererMediaSubsession::getStreamParameters()创建数据流通道。
- 与子sesssion对应的streamState实例,它的成员streamToken,是这个函数的一个参数。OnDemandServerMediaSubsession实例将它当做辅助数据结构,它被定义为类StreamState,与streamState只是首字母不同。
- 构造回复字符串。
以下是handleCmd_SETUP()的回复字符串。它的传输头部指定了收发两端的地址和端口,同时还指定了Session编号0x4F1FB7E7。
RTSP/1.0 200 OK\r\n
CSeq: 4\r\n
Date: Wed, Apr 29 2020 08:30:41 GMT\r\n
Transport: RTP/AVP;unicast;destination=192.168.2.128;source=192.168.2.133;client_port=63032-63033;server_port=6970-6971\r\n
Session: 4F1FB7E7;timeout=65\r\n\r\n
getStreamParameters()创建数据通道。
- 它的参数clientAddress指定了目标地址。这里因为请求中没有包括“destination=xxx”,所以这个地址是0。这时将目标地址指定ClientConnection的客户端地址。
- 调用createNewStreamSource()创建FramedSource实例。这是包括MP3FileSource在内的链,使用的MP3文件名为MP3AudioServerMediaSubsession的成员fFileName,在handleCmd_DESCRIBE()中已经指定为jzl.mp3。
- 创建用于RTP连接和RTCP连接的Groupsock实例。
- 从成员fInitialPortNum指定的起始端口开始,尝试可用的本地端口,作为RTP连接的本地端口。fInitialPortNum在OnDemandServerMediaSubsession的构造函数中,指定缺省值为6970。
- 调用createGroupsock()创建RTP连接的Groupsock实例。
- 如果不复用RTCP连接和RTP连接,则将RTCP连接的端口加1,创建RTCP连接的Groupsock实例。
- 为RTP连接,调用MP3AudioFileServerMediaSession::createNewRTPSink()创建RTPSink实例。
- 基于上述Groupsock和RTPSink实例,创建StreamState实例。
- 创建Destinations实例,保存RTP连接和RTCP连接的目标端口地址,这里分别是63032和63033。将Destination实例加入成员fDestinationHashTable中。
1.4.4 请求PLAY
PLAY的请求字符串如下。它请求jzl.mp3文件,ClientSession的编号是0xC47271EC。
这个session在之前处理SETUP请求时已经创建了。这里先用它查询先前创建的ClientSession实例,再调用RTSPClientSession::handleCmd_PLAY()。
PLAY rtsp://192.168.2.133:8554/jzl.mp3/ RTSP/1.0\r\n
CSeq: 5\r\n
User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)\r\n
Session: C47271EC\r\n
Range: npt=0.000-\r\n\r\n\
000port=53834-53835\r\n\r\n
RTSPClientSession::handleCmd_PLAY()的实现如下。
- 调用parseScaleHeader()和parseRangeHeader(),得到播放的起点和终点。
- 遍历成员fNumStreamStates,对其中streamState实例的成员subsession,调用OnDemandServerMediaSubsession::seekStream(),调整播放位置。
- 遍历成员fNumStreamStates,对其中streamState实例的成员subsession,调用OnDemandServerMediaSubsession::startStream(),开始播放。
OnDemandServerMediaSubsession::seekStream()的工作如下:
- 调用MP3AudioFileServerMediaSubsession::seekStreamSource()在媒体源中重新定位。
OnDemandServrMediaSubsession::startStream()的工作如下:
- 根据ClientSession的Id在成员fDestinationHashTable中查找对应的Destinations实例,其中保存了目标组播地址。
- 调用StreamState::startPlaying()开始播放。
- 这个StreamState实例是在处理SETUP请求时创建的, streamState结构的成员streamToken保存了这个实例。
- 调用OnDemandServerMediaSubsession::createRTCP()创建RTCPInstance实例。这里指定RTPSink实例,和RTCP连接的Groupsock实例作为参数。并调用setAppHandler()和setSpecificRRHandler()设置回调函数。
- 调用MediaSink::startPlaying(),开始解析媒体帧并通过RTP连接发送,同时RTCPInstance也通过RTCP连接发送RTCP信息。
以下是handleCmd_PLAY()的回复字符串。
RTSP/1.0 200 OK\r\n
CSeq: 5\r\n
Date: Wed, May 06 2020 07:59:07 GMT\r\n
Range: npt=0.000-\r\n
Session: C47271EC\r\n
RTP-Info: url=rtsp://192.168.2.151:8554/jzl.mp3/track1;seq=15637;rtptime=133202039\r\n\r\n
1.4.5 连接图
如下是最后得到的连接图。
- Live555服务器端IP地址为192.168.2.133,而客户端(这里使用VLC Media Player)IP地址为192.168.2.139。
- Live555创建TCP socket,绑定端口554或8554监听。
- VLC客户端本地端口xxx,连接Live555的TCP socket。后者接收请求,与客户创建TCP连接(用绿色标注),本地端口yyy。xxx和yyy由协议层自动确定。
- 客户端通过TCP连接发起请求,指定自己的RTP和RTCP连接的端口分别是65512和65513。
- 服务器端为RTP流和RTCP流分别创建UDP socket,本地端口分别为6970和6971。
- 服务器通过RTP流发送数据帧,通过RTCP流发送RTP流的信息。