FFServer源码分析

 FFServer源码分析
  @author FlyFire
  @copyleft
                                         
在本章将浏览ffserver的源代码,理解其设计的思路。重点研究ffserver对rtp rtcp的支持,研究ffserver管理多个连接的方法。
为使用rtsp管理多播,进行rtp rtcp的流媒体传输做准备。

在研究ffserver源码之前,我们需要理解ffserver的配置文件ffserver.conf。在ffserver.conf中透露了管理ffserver的蛛丝马迹。
ffmpeg\tests\目录下的ffserver.conf
MaxBandwidth指每个连接的最大带宽
Feed和Stream配置了该ffserver的输入和ffserver的输出。Feed是一个ffserver获得流的地方。可以是从一个ffmpeg的encoder或者另一个ffserver或 者是一个编码好的文件。每个Feed中包含一个video和/或一个audio。
定义每个输出的流。流的格式 帧率 来源 GOP 等。

现在分析ffserver.c
1. main()
首先解析了配置文件,打开指定的文件流
然后创建子进程,并在子进程中执行http_server
2. http_server
a.打开ffserver的监听端口
b.打开rtsp的监听端口
c start_multicast
  .为各个需要多播的流创建相应的multicast ip 和 multicast port。
  并初始到各个multicast组的rtp连接的context。
  所谓初始相应的rtp连接的Context是指 分配HttpContext
  在HttpContext中指明相应连接的protocol state sessionId。
  //这里注意当HttpContext中使用的是本地avi文件,它的域是怎样初始化的
 
  注意在这里,只要是组播就一定使用rtp rtcp协议。所以在start_multicast()中,初始到各个multicast组的rtp连接的context。
  如果当前的流是multicast,
  C0创建该流的HttpContext
              rtp_new_connection()
              初始化该HttpContext对应的from_addr sess ion_id 和 protocol
       将该HttpContext加到由first_http_ctx引导的链表中
  C1.对该流的HttpContext执行open_input_stream
  C10. av_open_input_file 打开相应的输入文件
在av_open_input_file中,对相应的输入文件的格式进行Probe。并将Probe的结果,用以初始化HttpContext中的AVInputFormat。
当打开一个本地文件时,常常是根据后缀名进行Probe的。可以看到,在av_open_input_file中将AVProbeData中的buf域初始化为空。
在进行Probe时,先通过执行各个AVInputFormat中的read_probe函数,若没法进行read_probe,则采用后缀名的比较方法。
AVProbeData中的buf域何时会被初始化呢?
在跟踪时发现,av_read_packet中有对AVProbeData中的buf进行初始化的代码。
在av_open_input_file中,先没有对AVProbeData做任何初始化就开始Probe。意味着仅仅通过filename的后缀名进行Probe。如果通过后缀名Probe失败,在av_open_input_file中先url_fopen然后再次Probe。
Probe完成后调用av_open_input_stream(),使用Probe到的AVInputFormat打开相应的文件流
在av_open_input_stream中调用相应的AVInputFormat的read_header()获得特定文件的基础信息,放在AVFormatContext的pri_data中,给后面解码时使用。

也就是说,在av_open_input_file中完成了对AVFormatContext的初始化和AVInputFormat的初始化。AVFormatContext中已经包括了打开文件的头部信息,供解码时使用。而AVInputFormat则在Probe时完成了初始化。
                     从而完成了对HttpContext中的fmt_in和stream->ifmt的初始化。
                    
       C2.
              对HttpContext执行rtp_new_av_stream()
              C20
               rtp_new_av_stream()
在其中分配一个rtp连接所需的上下文。分配一个RtpContext并赋给URLContext。其中包括该rtp连接使用的两个端口 ttl等信息。HttpContext中的rtp_handles[]即在此初始。每个rtp stream对应一个rtp_handle。该rtp_handle也就是URLContext中。
对HttpContext中的每个Stream,每个Stream中的更小的stream分配管理该小stream的rtp_handle。
       C3.
              将该HttpContext的状态设为HTTPSTATE_SEND_D ATA。
      
    
d.对server_fd和rtsp_fd进行poll,看是否有数据可读。如果有,则新建相应的HttpContext。.
  并加入到first_http_ctx中。设置HttpContext时,如果是rtsp端口的连接,设为RTSPSTATE_WAIT_REQUEST;如果是serverfd的连接,设为HTTPSTATE_WAIT_REQUEST

e.处于WAIT_REQUEST状态下的HttpContext使用相应的poll_entry来进行管理。
然后依次对每个HttpContext进行handle_connection。
  handle_connection()根据每个HttpContext的State以及相应的poll_entry进行处理。
  注意:实际的状态变迁数据读取都是在handle_connection中完成的。
  在进行rtsp的SETUP时,调用rtp_new_connection创建了rtp连接的HttpContext。且注意rtp连接的HttpContext的初始状态为HTTPSTATE_READY。并将该HttpContext加入到first_http_ctx的链表当中。
  重点理解rtsp是怎样管理多个rtp的connection。      

  f.在rtsp_cmd_setup中,有个地方让人疑惑:检查传入的RTSPMessageHeader中的session_id。按我的理解,session_id应该是在SETUP时才产生的,在此次的rtsp_cmd_setup中,应该无需检查,直接分配才对。令人奇怪的是,竟然根据该session_id去查找相应的rtp session。对于第一次SETUP而言,应该是没有相应的rtp session的。为何还要做这样的检查?
  最不能理解的是在rtsp的wiki上,While HTTP is stateless, RTSP is a stateful protocol. A session identifier is used to keep track of sessions when needed; thus, no permanent TCP connection is required。是不是意味着rtsp的tcp连接能随时断开?断开后再通过服务端的session_id进行恢复?
g.在执行完rtsp_parse_request之后,该HttpContext状态变成了RTSPSTATE_SEND_REPLY。
  这里可能需要注意,在rtsp的监听端口上,当来了一个新的客户端连接时,则新建一个针对该连接的HTTPContext。这样每个客户端由一个RTSP的HTTPContext控制状态。它控制多个RTP流?还是说只控制一个RTP流?像AVI文件的传输,有video流audio流和text流。它们是使用一个RTSP控制还是三个RTSP控制?显然一个AVI文件三个流是相关的,如果要快进,三个流都要快进;如果要暂停,三个流都要暂停。所以使用一个RTSP连接管理三个RTP流。只进行一次SETUP,但建立三个RTP connection?????
  这个问题现在相当不清楚。。。。。。
  好像是这样的,在ffmpeg的rtsp_cmd_setup中,貌似是 每个流进行了一次SETUP。也就是说,如果是个avi文件,需要进行三次SETUP,分别建立音频流视频流和字幕流。再第一次SETUP时分配相应的RTP的HTTPContext,以后两个SETUP,共用已经建立的该HTTPContext。在rtsp_cmd_setup中,在第一次SETUP命令时,执行rtp_new_connection(),以后两次执行rtp_new_av_stream()。
执行完相应的rtsp_cmd_xxx之后,通过url_close_dyn_buf完成了解析后产生的应答的传送。
 
//先研究RTSP对多个RTP RTCP的管理。
//之后研究RTCP对RTP的管理
h.这里不得不提到,在流媒体服务器端,对ffserver而言,它仅解析对每个文件中的某个流的请求,并不解析对整个文件的请求。即在ffserver配置文件中,针对每个具体单一的audio/video流配置。每个FFStream包含多个相关的单一的流audio/video/text
  由客户端将这些流合并起来。
  一个rtsp的session可以管理多个相关的流。
  Rfc2326中的内容:
     Note that a session identifier identifies a RTSP session across
   transport sessions or connections. Control messages for more than one
   RTSP URL may be sent within a single RTSP session. Hence, it is
   possible that clients use the same session for controlling many
   streams constituting a presentation, as long as all the streams come
   from the same server. (See example in Section 14). However, multiple
   "user" sessions for the same URL from the same client MUST use
   different session identifiers.

   The session identifier is needed to distinguish several delivery
   requests for the same URL coming from the same client.

Rfc2326的例子(Section 14),
Client C requests a presentation from media server M . The movie is
     stored in a container file. The client has obtained an RTSP URL to
     the container file.

         C->M: DESCRIBE rtsp://foo/twister RTSP/1.0
                     CSeq: 1

         M->C: RTSP/1.0 200 OK
                     CSeq: 1
                     Content-Type: application/sdp
                     Content-Length: 164

                     v=0
                     o=- 2890844256 2890842807 IN IP4 172.16.2.93
                     s=RTSP Session
                     i=An Example of RTSP Session Usage
                     a=control:rtsp://foo/twister
                     t=0 0
                     m=audio 0 RTP/AVP 0
                     a=control:rtsp://foo/twister/audio
                     m=video 0 RTP/AVP 26
                     a=control:rtsp://foo/twister/video

         C->M: SETUP rtsp://foo/twister/audio RTSP/1.0
                     CSeq: 2
                     Transport: RTP/AVP;unicast;client_port=8000-8001

         M->C: RTSP/1.0 200 OK
                     CSeq: 2
                     Transport: RTP/AVP;unicast;client_port=8000-8001;
                                           server_port=9000-9001
                     Session: 12345678

         C->M: SETUP rtsp://foo/twister/video RTSP/1.0
                     CSeq: 3
                     Transport: RTP/AVP;unicast;client_port=8002-8003
                     Session: 12345678

         M->C: RTSP/1.0 200 OK
                     CSeq: 3
                     Transport: RTP/AVP;unicast;client_port=8002-8003;
                                           server_port=9004-9005
                     Session: 12345678

         C->M: PLAY rtsp://foo/twister RTSP/1.0
                     CSeq: 4
                     Range: npt=0-
                     Session: 12345678

         M->C: RTSP/1.0 200 OK
                     CSeq: 4
                     Session: 12345678
                     RTP-Info: url=rtsp://foo/twister/video;
                         seq=9810092;rtptime=3450012

         C->M: PAUSE rtsp://foo/twister/video RTSP/1.0
                     CSeq: 5
                     Session: 12345678

         M->C: RTSP/1.0 460 Only aggregate operation allowed
                     CSeq: 5

         C->M: PAUSE rtsp://foo/twister RTSP/1.0
                     CSeq: 6
                     Session: 12345678

         M->C: RTSP/1.0 200 OK
                     CSeq: 6
                     Session: 12345678

         C->M: SETUP rtsp://foo/twister RTSP/1.0
                     CSeq: 7
                     Transport: RTP/AVP;unicast;client_port=10000

         M->C: RTSP/1.0 459 Aggregate operation not allowed
                     CSeq: 7
  这个例子中,可以清楚地看到对一个同时存储了音视频流的文件,Client端使用了一个rtsp的session,针对不同的(video/audio)流,进行了两次SETUP。在两个SETUP完成后,以后的PLAY PAUSE 和TEARDOWN都是针对这个session的,同时对这个session包含的两个流进行操作。这样也就理解了上午遇到的问题,为何有一个SETUP的cmd过来时,它会先检查该session是否存在。因为一个session中可以包含多个相关的流,而每个流都需要进行一次SETUP。
 
 
  对照ffplay.c中的代码,在函数decode_thread中通过调用AVFormatContext中的AVInputFormat的read_play方法,(见Ffplay.c 2050行 av_read_play()),完成了相应的AVInputFormat的play。对于使用rtsp的实时播放,对应rtsp_demuxer。
  rtsp_demuxer在rtsp_read_header中,先通过rtsp_send_cmd发送OPTIONS命令,再发送DESCRIBE命令,再通过调用make_setup_request完成了对AVFormatContext中包含的多个流分别建立连接,发送SETUP命令。这样在play的时候,就可以对整个session发送play命令。
 
  i.继续阅读ffserver源码,看它怎样管理rtp rtcp。尤其是这其中涉及的状态变迁。
  在某个rtsp连接,遇到第一个SETUP命令时,执行了rtp_new_connection(),然后再rtp_new_av_stream。以后的几次SETUP,执行的都是rtp_new_av_stream。也就是说,假如有一个avi文件,涉及audio video text三个流,那么仅通过一个HttpContext来处理。虽然有三个rtp传 输(audio video text使用三个不同的rtp流进行传输,同时还要有三个不同的rtcp流)。
   I0
在rtp_new_connection中,将该HttpContext的session_id设置成由rtsp的第一次SETUP时产生的session_id。状态设置成HTTPSTATE_READY,并加入由first_http_ctx引导   的链表中。
  I1
在rtp_new_av_stream中,为相应的流建立了URLContext,并将该URLContext赋给该connection(对应一个session)的rtp_handles(参见ffserver 3215行)。同时为相应的rtp流建立一个AVFormatContext,同时将该AVFormatContext赋给该HttpContext的rtp_ctx数组
这里提一下两个函数:url_open_dyn_packet_buf()和url_close_dyn_buf()
url_open_dyn_packet_buf()用来创建相应的ByteIOContext,并初始化其中的read write函数,和缓冲区。ByteIOContext的write,不过是将一个缓冲区中的内容写到自己的缓冲区中。在url_close_dyn_buf中,执行了put_flush_packet,在put_flush_packet中完成了从ByteIOContext的buffer将数据copy到ByteIOContext的opaque中。
       I2
对于已有的HttpContext可以通过poll查询是否有数据到来(针对tcp的socket可以)。UDP连接是怎样读取数据的?这个涉及到对rtsp_cmd_play的分析
  I3
一个rtsp连接可以管理多个session。每个session包括一组相关的流的传输。同时每个session有一个自己的HttpContext(除了被这个大的rtsp的HttpContext管理外),在该HttpContext中包含了自己的session_id。
  I4
对于每个play的命令,rtsp连接先在所有的HttpContext中查找到相应session的HttpContext,设置该HttpContext的状态为HTTPSTATE_SEND_DATA。然后返回确认信息。
       I5
在http_server()检查是否有新的连接,并更新连接的状态。同时,对每个连接进行处理,调用handle_connection()。当某个连接的状态是HTTPSTATE_SEND_DATA时,在handle_connection()函数中,通过执行http_send_data(),完成数据的发送
  I6
  http_send_data()中先调用http_prepare_data,从文件中准备数据。这里牵涉到从一个文件中读出三个流的问题。
  从文件中将流读出来,并赋给了一个AVPacket。现在重点研究该AVPacket的结构,用它来理解发送三个流(video audio text)并同步的方法。
  在http_prepare_data中,先通过av_read_frame()从该session HttpContext对应的输入中读取一帧,见Ffserver.c 2136行。根据该帧对应的流,将该帧写到相应的流的AVFormatContext中。相应的流的AVFormatContext调用该流对应的AVOutputFormat的write_packet方法。这里是rtp_muxer的rtp_write_packet方法。将数据从一个缓冲区写到另一个缓冲区。
通过http_prepare_data()将要发送的数据放到该session的HttpContext的缓冲区中。涉及到从相应的文件中读取一帧并将该帧放到正确的流的缓冲区中。
  然后根据发送时间确认是否要发送。get_packet_send_cloc k()-get_server_clock()。如果时间未到,不发送
  最后通过url_write()使用相应的流的URLContext将该session的HTTPContext的缓冲区中的数据发送出去。
  一个需要注意的地方,在rtp_protocol的rtp_write中,先分析了存储在缓冲区中的数据,确认类型是rtp的还是rtcp的,从URLContext中的priv_data获得RTPContext。获得相应的rtp_hd或rtcp_hd。再通过url_write()操作缓冲区的内容。由于rtp_hd和rtcp_hd这两个URLContext的URLProtocol都被初始化为UDPProtocol,这里即采用UDPProtocol发送缓冲区中的内容。
 
  返回再去看rtsp_cmd_setup时为每个流建立的URLContext。每个流的URLContext有一个RTPProtocol类型。且用该RTPProtocol的url_open继续初始化该URLContext。
  即使用rtp_open对URLContext做进一步的初始化。在该rtp_open中,使用RTPContext初始化了该URLContext的priv_data部分。同时,RTPContext中的rtp_hd和rtcp_hd这两个URLContext采用UDPProtocol初始化。
 
j.现在再去看http_prepare_data部分。研究管理该session的HttpContext怎样读取文件中的多个流,怎样与rtcp的sr和rr交互,怎样控制传输速率,怎样用rtp重新打包读出的文件中的帧。
在http_prepare_data中,先通过av_read_frame()读取相应session的HttpContext对应的输入文件,读入一个AVPacket,即av_read_frame(c->fmt_in,&pkt)。
而实际的控制操作,与rtp rtcp交互,是通过av_write_frame()将读取到的AVPacket写入对应的stream的AVFormatContext的缓冲区中。
在av_write_frame中调用相应AVFormatContext的AVOutputFormat的write_packet方法。这里每个流的AVFormatContext的AVOutputFormat在rtsp_cmd_setup时调用new_av_stream()中被初始化为rtp_muxer,所以在执行rtp_muxer的write_packet即rtp_write_packet()。
   rtp_write_packet中,将一个AVPacket写到相应的AVFormatContext的缓冲区中。
   在这里使用到了AVFormatContext中的priv_data。将该值赋给了RTPMuxContext。
而AVFormatContext中的priv_data是在rtp_new_av_stream中,见ffserver.c的3236,通过av_set_parameters分配的。在av_set_parameters中通过检查该AVFormatContext的AVOutputContext的priv_data_size来决定是否需要在AVFormatContext中分配priv_data部分。也就是说,AVOutputContext的MuxContext是存放在相应的AVFormatContext的priv_data部分的。
   rtp_write_packet中可以发现,rtp数据包的统计由RTPMuxContext完成。
   在这里同时发送了发送报告,通过rtcp_send_sr()。(并非立即发送,而是将sr写入到相应的AVFormatContext的缓冲区中)
   同时,rtp流每个数据包的包装,加入ssrc的头部和时间 载荷标志都是通过RTPMuxContext完成的。因为这些信息都存储在RTPMuxContext中。
  k.上面有一点需要注意,因为是将该写入相应的AVFormatContext的缓冲区中,数据信息和sr都放在一个缓冲区中,在发送时根据载荷类型,使用该AVFormatContext的URLContext中的URLProtocol中的priv_data。该priv_data中有两个URLContext,分别是hd和hdc,选择相应的hd hdc发送。
  L.再次考虑是怎样读取client端的rr,并通过该rr来控制发送包的速度。
     同时可以学习client端怎样处理接收的rtp包进行重排的。
     已经分析了ffserver怎样处理rtsp的请求的,包括发送请求等。现在需要理解ffserver怎样处理通过udp传来的数据包,怎样通过客户端的rr控制传输速率
     在上面分析http_prepare_data(),我们可以知道,在从文件读取数据到该session的HttpContext的缓冲区时,使用了rtp_muxer的rtp_write_packet方法。rtp_muxer将文件中的数据读出来,然后根据rtp的方式打包,包括判断是否到发送sr的时候,如果是,则将sr打包到缓冲区中;给从文件中读出的帧加上头部,包括时间戳 ssrc,序列号和载荷类型等。
    
     在http_prepare_data完成后,调用了url_write()方法,完成数据的发送。
     在url_write中,调用了相应的URLContext的URLProtocol的url_write方法。这里对应rtp_protocol的rtp_write()方法。
     在rtp_write()中,获取该URLContext的priv_data部分,赋给RTPContext,然后根据载荷类型,获取该RTPContext中相应的URLContext,然后再对相应的URLContext执行url_write方法。
 
  但我没法找到ffserver接收客户端发来的rr并进行处理的程序段。
  我怀疑是在创建URLContext时,初始化其中的RTPContext时, 需要创建rtp_hd rtcp_hd,创建完成后开始了read()的操作。
  再次查看时并没有如想象中那样操作。
  截止2010-4-22下午2时13分我仍然没有解决这个问题。如果有网友知道,可以mail我:[email protected]

 欢迎技术讨论。


原文:http://blog.sina.com.cn/s/blog_51396f890102dxds.html

你可能感兴趣的:(session,Stream,cmd,url,input,audio)