一、 rtsp 协议说明
rtsp的协议层级
- rtsp 属于应用层, 使用tcp传输,主要是传递服务器的一些信息,实现流连接。播放 暂停 销毁等控制
- rtp 实现音视频数据包的发送,通过RTSP等协议的SDP信息协商好了RTP数据包的发送目的和传输方式,我们就需要把音视频数据打包成RTP包,用UDP或者tcp发送给接收端了。
二、ffmpeg 中的实现
1. rtsp_read_header
其中ff_rtsp_connect连接服务器, 首先会指定下一层使用的协议是什么? 可以指定rtp over tcp 或者 rtp over udp。由这个lower_transport_mask 决定的,rt->lower_transport_mask 会有一个默认值。 通过ff_rtsp_options[] 进行设置的
{ "rtsp_transport", "RTSP transport protocols", OFFSET(lower_transport_mask), AV_OPT_TYPE_FLAGS, {.i64 = 0}, INT_MIN, INT_MAX, DEC|ENC, "rtsp_transport" }, \
默认是0 udp的形式。
2. ffurl_open
ffurl_open(&rt->rtsp_hd, tcpname, AVIO_FLAG_READ_WRITE,&s->interrupt_callback, NULL) < 0)
- 调用到了tcp_open(), tcp去连接,tcp调用 socket() 创建tcp的fd。
- 1 调用connect 去连接服务器,根据不同的返回值 进行不同的处理。
3. ffurl_connect
ffurl_connect(rt->rtsp_hd_out, NULL)
ff_rtsp_send_cmd(s, "OPTIONS", rt->control_uri, cmd, reply, NULL)
- 第一步: 连接tcp,发送options这个tcp的连接主要负责服务器交互命令的传输如:option play discribe等
- 根据传进来的url,构建tcp的地址进行连接(地址放在rt->control_uri这个变量中),并创建一个rt->rtsp_hd 的context,从这个context 中获取到 tcp_fd。第一个发送的命令是 OPTIONS, 服务器会返回可以运行的那些命令。
4.ff_rtsp_setup_input_streams
第二步:向服务器发送discribe的消息, 然后解析获取到的sdp文件,根据文件建立流,获取音视频流的信息。
发送discribe 信息 ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, &content);
服务器回复一个sdp文件,里面包含地址,range,track等信息。
解析sdp文件
其中m开始的这一行就是流的信息。流的信息会保存到rt->rtsp_streams 中
rtsp_st = av_mallocz(sizeof(RTSPStream));
if (!rtsp_st)
return;
rtsp_st->stream_index = -1;
dynarray_add(&rt->rtsp_streams, &rt->nb_rtsp_streams, rtsp_st);
5.ff_rtsp_make_setup_request
- 进行rtp 和 rtcp的连接 ,连接的地址是 从url的host中获取到,比如rtsp的ip rtsp://210.13.9.10:1554 其中port取一个随机值,并且rtp 和 rtcp 的port 只相差1。创建rtp_handle,这个ffurl_open 调用到了rtp_open 、rtp_open:分别创建两个udp的地址,rtp和rtcp的地址就是port相差1,如下,然后分别创建 s->rtcp_hd和 s->rtp_hd 这两个handle
udp_open: uri = udp://210.13.9.10?localport=13054&fifo_size=0
udp_open: uri = udp://210.13.9.10:0?localport=13055&fifo_size=0
- 连接完成之后,发送setup的命令到服务器ff_rtsp_send_cmd(s, "SETUP", rtsp_st->control_url, cmd, reply, NULL);
- 发送完成之后 ,做一些open context的操作,实际上是ff_rtsp_open_transport_ctx调用ff_rtp_parse_open 就是根据code_id来看 读取上来的数据是不是需要parse
- 如果payload_type 是MP2T, ts流的情况,还需要ff_mpegts_parse_open,打开mpegts的一些 pmt pat pes的回调函数。
6. rtsp_read_play
- 发送play的命令到服务器进行播放操作,服务器就会发送数据到客户端ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
7. rtsp_read_packet
- 调用 ff_rtsp_fetch_packet,udp的情况 udp_read_packet 实际是调用的是ret = ffurl_read(rtsp_st->rtp_handle, buf, buf_size); 这个调用的是rtp_read。
- 这里分别从两个fd那边来获取数据 一个是rtcp_fd, 一个rtp_fd.读到底层buf之后 调用ff_rtp_parse_packet(rtsp_st->transport_priv, pkt, &rt->recvbuf, len);
- 如果是ts的话,那么会调用tsparse 先parse一下 在调用codec的parse parse 一下 然后返回。
三、rtsp_read_packet 序号处理
- 读取数据流程: 正常没有丢包和乱序的情况下,从udp_read 中读取一包出来,读取的方式是采用poll等待, fd 有两个一个是rtp实际上下面一层是rtpproto.c 调用的rtp_read_packet, 没有调用udp_read. 在rtp_read_packet里面就直接调用系统的recvfrom 从之前的open的fd 中读取数据回来。
- 这个数据经过解析回到上面一层也就是rtsp。 这里面就有对序号不连续的情况进程处理。一个是rtcp,有数据可以读的话 就里面读上来, 然后这边读上来的包立马就进行parser,parse的时候如果这一包 还没有销毁玩 要继续调用parse。
- 第一包 或者 解析到的seq 是连续的,那就直接调用parse
-
seq不连续,比如1 3这样,那么把3给存储起来 放到queue里面。同时更新wait_end 为queue里面第一包放进去的时间+能够延迟最长的时间默认为100ms。
queue是一个队列,第一个元素是最早放进来的,后面的依次放到后面。 这边又分为两种情况
-
读超时, 也就是2这个数据一直没读到,后面读的是3,4,5,6,7这样,那么就最多读取100ms、超过了就返回超时(这个时候出现log max delay reache )。然后强制调用ff_rtp_parse_packet(rtsp_st->transport_priv, pkt, NULL, 0)。通过倒数第二个参数NULL控制会从queue的队列里面读数据 进行parsre(这个时候出现log RTP: missed)然后返回。
-
读到了2这个数据,那么判断序号seq 和 queue的seq就相差1 也就是seq为2 ,queue第一个元素是3.直接将2parse。parse出来之后,有一个while循环会把queue里面的数据全部parse掉。parse掉之后才会继续读取数据。同时将wait_end 重置为0,也就是说恢复到没有包丢掉的情况。
-
如果是直接rtp地址,就少了rtsp的处理。就看rtpproto.c里面的处理 直接就是rtp_read_header 建立udp或者tcp的连接,得到一个fd,然后从这个fd读数据。如果是直接udp的地址,同样少了rtp的处理, 直接就看udp.c的处理。
四、rtsp丢包原因分析
分析方法:rtsp丢包可能发生在传输的每个环节,一般来说最大的可能是服务器端发送的时候某些包没有发送 或者 网络环境导致发送的包丢失 或者客户端应用程序导致底层收的包没有及时处理。通常通过分段的网络抓包可以确认包是在哪个阶段丢的
- 问题:用tcpdump抓包看是不丢包的,但是从udp read上来的数据 解析成rtp包是丢失的,也就是rtp的序号是不连续的,即使是加大乱序buffer 到很大也是没有效果的。 udp 包乱序了基本上后面就再也读不上来了。
- 首先tcpdump工作在哪一个层次,tcpdump工作在IP层。 tcpdump出来的是正确的,就说明网络层是正常。 只有应用层去接收数据的时候丢掉了。分析 是不是读数据不及时, 看读数据的线程,cache 满的时候 是有一个sleep的操作。 也就是当上层cache 很多的时候,会减慢读取数据的线程。通过分析 丢包基本上都是发生在读数据线程sleep很久的时候。数据cache多的时候,不去减慢读数据,也就是不去做sleep 操作。 相当于有一个线程一直在及时的把数据从socket读上来。这个时候看是正常的。 那么就说明之前是cache 导致socket的数据堆积 变慢了。
- 如果从客户端网络抓包看是不丢包,但应用却丢包,那么基本上是应用处理接收到的网络包程序有问题。udp是无连接,客户端没有及时接收那么缓冲去满的话就会丢弃,但这些数据服务端是实际都发送成功 并且被IP层接收了。
五、相关调试命令
ifconfig etho1 看是不是有丢包