# 09|RTP & RTCP:如何正确地将视频装进RTP中? 你好,我是李江。 在前面的课程中,我们详细地讲述了视频编码的原理以及预测编码和变换编码的知识。通过这些我们了解了视频编码的基本原理和步骤。同时,我们还用了一节课的时间深入探讨了H264的码流结构,相信你已经清楚了H264码流是什么样的,以及如何从码流中分离出一帧帧图像数据,并学会了如何判断这些帧的类型。 那么从这节课开始呢,我们就要进入视频传输和网络对抗部分了。我们会在视频编码码流的基础上,讲讲如何将码流打包成一个个数据包发送到网络上,并进一步讨论如何避免在发送的过程中引起网络拥塞,从而保证视频的流畅性。同时,我们会进一步在后面的课程中讲解如何在网络不断变化的时候做好视频码控算法,如何防止视频出现花屏,以及如何尽量减少视频卡顿等非常有难度的实际工程问题。 这些问题是视频开发过程中经常会遇到且迫切需要解决的重要问题。而解决这些问题的基础就是需要熟悉RTP和RTCP协议,也就是我们这节课的重点。 接下来我们会分别从RTP协议、RTCP协议和H264的RTP打包方法这三个方面来展开这节课。首先让我们一起来认识一下RTP协议。 ## RTP协议 RTP(Real-time Transport Protocol)协议,全称是实时传输协议。它主要用于音视频数据的传输。那它的作用是什么呢? 一般我们在实时通信的时候,需要传输音频和视频数据。我们通常是这样做的,先将原始数据经过编码压缩之后,再将编码码流传输到接收端。在传输的时候我们通常不会直接将编码码流进行传输,而是**先将码流打包成一个个RTP包再进行发送**。 那为什么需要打包成RTP包呢?这是**因为我们****的****接收端要能够正确****地****使用这些音视频编码数据,不仅仅需要原始的编码码流,还需要一些额外的信息**。比如说: * **当前视频码流是哪种视频编码标准**,是H264、H265、VP8、VP9还是AV1呢?我们知道每种不同的编码标准,其码流解析的方式肯定也不一样。这个就需要通过RTP协议告知接收端。 * 当我们知道编码标准了,我们就可以正确地解析码流,并解码出图像了。但是我们又会遇到一个新的问题,那就是**按照什么速度播放视频呢?**这个也需要RTP协议告知接收端。 这就是RTP协议的一个重要的作用,即告知接收端一些必要的信息。当然RTP协议的作用不止这些,它其实在网络带宽预测和拥塞控制的时候也发挥出了至关重要的作用。我们在之后的课程中会继续讨论,这里就先不讲了,你大体有个印象就可以。 我们知道RTP包需要附带很多额外的信息,那这些信息在RTP包中是怎么存在的呢?其实RTP包包括两个部分:第一个部分是RTP头;另外一个部分是RTP有效载荷。其中RTP头主要是用来携带前面说的那些额外信息的,等会儿我会详细介绍一下RTP头部每个字段的意义。 这里我先稍微跟你解释一下另外一个部分,也就是RTP有效载荷。RTP有效载荷,其实就是RTP包里面的实际数据。如果是H264编码打包成RTP包,那有效载荷就是经过H264编码的码流;如果是VP8编码呢,那就是VP8码流。 接下来,我们重点来看看RTP包的头部。具体如下图所示: ![](https://static001.geekbang.org/resource/image/14/e6/143a86a6aef7f664beca7268f58e99e6.jpg?wh=1280x426) 是不是有点懵,别急,下面我给了一张表格,你可以对照着表格看看RTP包头的每一个字段占用的位数和具体的含义。其中绿色部分是很重要的知识点,需要你重点掌握。 ![图片](https://static001.geekbang.org/resource/image/3b/52/3b724acdcb5a69f3c1831591be49ca52.png?wh=1354x1194) 上面讲的就是RTP头部的主要组成部分。在这里需要单独提一下RTP头部的另外一个比较重要的部分,就是RTP扩展头。从上表我们可以看到,RTP包头有一个扩展头标志位X,当扩展头标志位X为1的时候,说明有RTP扩展头。RTP扩展头由于平时大家很少用看似不怎么重要,但是在RTC场景中,尤其是WebRTC中经常会用到。另外,RTP扩展头我们在带宽预测的时候也会用到。所以建议你也了解一下。 扩展头主要是用来给用户自定义扩展使用的。因为协议是标准的,但是用户使用场景却是多种多样的,所以RTP需要考虑的比较全面,留了一个扩展头可以让用户根据使用场景和需求,自己定义扩展头,用来传输需要在RTP包中传输的信息。扩展头的格式可以参考[这篇文章](https://www.cnblogs.com/ishen/p/12050077.html),这里就不做过多的展开了。 好了,以上就是RTP协议的主要知识点。有了RTP协议,我们就能够将码流打包成RTP包发给接收端了。如果你只负责传输RTP包,而不需要管传输过程中有没有丢包,以及传输RTP包的时候有没有引起网络拥塞的话,那你只需要使用RTP协议就可以了。比如说,你选择使用TCP协议传输RTP包的话就可以不用管这些事情,因为TCP协议具有丢包重传、拥塞控制等功能。 但是通常情况下,我们在传输音视频数据的时候不会使用TCP协议作为传输层协议。这是因为TCP协议更适合传输文本和文件等数据,而不适合传输实时音频流和视频流数据,所以我们通常会**使用UDP协议作为音视频数据的传输层协议**。但UDP协议不具有丢包重传和拥塞控制的功能,需要我们自己实现。那怎么办呢? 其实,真要做好丢包重传和拥塞控制是非常难的,一节课也讲述不清楚,所以,我们会在接下来的好几节课里详细解释。接下来我们可以先关注下丢包重传和拥塞控制的基础之一,也就是RTP协议的“好兄弟”,RTCP协议。 ## RTCP协议 RTCP(Real-time Transport Control Protocol)协议,全称是实时传输控制协议。它是辅助RTP协议使用的。RTCP报文有很多种,分别负责不同的功能。常用的报文有发送端报告(SR)、接收端报告(RR)、RTP反馈报告(RTPFB)等。而每一种报告的有效载荷都是不同的。我们就是通过这些报告在接收端和发送端传递当前统计的RTP包的传输情况的。我们使用这些统计信息来做丢包重传,以及预测带宽。 不过,我需要再次强调一下,**RTCP协议只是用来传递RTP包的传输统计信息,本身不具有丢包重传和带宽预测的功能,**而这些功能需要我们自己来实现。 我们上面讲到了RTCP协议有很多种报告,而每种报告其实定义的具体内容都是不一样的。我们这里以RTPFB报告中的NACK报告(丢包提示报告)作为一个例子来看看RTCP协议大概是什么样子的。(RTPFB报告包含了多种子报告,NACK报告只是其中的一种,因为我们后面还会用到这个报告,所以这里我们就先以这个报告为例子。) 下图就是NACK报告的协议格式。 ![](https://static001.geekbang.org/resource/image/83/cf/8346b7d062d6e18c1700603a2dd1a5cf.jpg?wh=1280x434) 其中,每一个字段在下表中都有详细的解释。 ![](https://static001.geekbang.org/resource/image/c3/73/c3e2f8185a38c0746be0c0dbd6b52073.jpg?wh=1280x720) 从上面的NACK报告我们可以看到,RTCP协议跟RTP不同,它们传递的东西是不一样的。 我们知道RTP是用来传输实际的视频数据的。它就像一个快递盒,先装好视频,然后填好运送的视频基本信息和收件人信息,最后将视频运送到收件人手上。 而RTCP协议则像是一个用来统计快递运送情况的记录表。其中的NACK报告就是快递丢件情况的记录表。它记录着哪些快递丢了。发件人收到了NACK之后,可以重新寄一个同样的快递给收件人,防止收件人没有收到快递。在这里也就是将丢失的视频RTP包重传一遍。 虽然我们只讲了一种RTCP报告,但是其它的报告也是类似的。大多数报告都是用来记录传输信息的。因为数量很多,我们这里就不一一展开了。如果你有兴趣的话,可以查看这个[RFC文档](https://datatracker.ietf.org/doc/html/rfc3550)。 好了,通过学习RTP和RTCP的基础知识,我们了解了RTP包的协议格式和主要负责的功能,也知道了RTCP的协议格式和其主要承担的责任。接下来我们就进入实际工程部分的知识了。 我相信通过前面课程的学习,你对H264的码流结构已经较为熟悉了,H264是在工程中用得比较多的编码标准,所以这里我们以H264为例来讲讲实际工程开发中,我们怎么将H264码流打包成RTP包。 ## H264 RTP打包 我们前面说了,H264码流是放在RTP的有效载荷部分的。因此有效载荷前面的RTP头部跟码流本身是没有关系的,所以我们可以直接先将头部的字段填好就可以。接下来我们需要将H264码流填充到RTP有效载荷中去。 **RTP H264码流打包分为三种方式:分别是单NALU封包方式、组合封包方式、分片封包方式。**顾名思义,单NALU封包方式是一个NALU打一个RTP包;而组合封包方式就是多个NALU打一个RTP包;分片封包方式则是一个NALU分开放在连续的多个RTP包中。下面我们来分别看一下各种打包方式是怎么样的。 1、单NALU封包方式 单NALU封包方式非常简单。我们在RTP头部的后面,直接放置NALU数据即可。注意,根据RTP的规定,这里需要**将NALU数据前面的起始码去除****,**不要将起始码也带入RTP包中。其格式如下: ![](https://static001.geekbang.org/resource/image/12/91/12961a82dca32ea3f1d760b4674fbf91.jpg?wh=1280x570) 为了让你更直观地理解这种打包方式,我给出了打包的示意图。具体如下所示: ![](https://static001.geekbang.org/resource/image/3d/9b/3d4845a261ef2f7e0683d8a81522ab9b.jpg?wh=1280x504) 这种打包方式适合于单个RTP包小于1500字节(MTU大小)的时候。一般来说,一些P帧和B帧编码之后比较小,就可以使用这种打包方式。 2、组合封包方式 组合封包方式稍微复杂一些。它是将多个NALU放置在一个RTP包中。在RTP头部之后,且放置NALU数据之前,我们需要放置一个1字节的STAP-A的头部。其中,STAP-A Header跟NALU Header的格式是一样的,只是Type字段的值不一样。因此,你可以参考H264码流结构课程中NALU小节来理解STAP-A的头部的格式。具体如下图所示: ![](https://static001.geekbang.org/resource/image/16/c2/16ec78c30e2760fab5a34a74524663c2.jpg?wh=1280x406) 其中,Type的取值如下表所示。这里我需要提醒你一下,表中的24和25类型就是STAP组合封包方式。注意,我们这里只讲STAP-A,这是因为STAP-B很少用到。 ![](https://static001.geekbang.org/resource/image/5a/a6/5a27c25b87c3fcb2be2a396f59ecb2a6.jpg?wh=1280x664) 放置完STAP-A Header之后,在每一个NALU的前面我们需要放置一个2字节的size字段,用于表示后面的NALU的大小。之后才是NALU的数据。记住同样需要**去掉起始码**。其格式如下: ![](https://static001.geekbang.org/resource/image/bb/c3/bb5498a2d629d5c754920d62d401c5c3.jpg?wh=1280x720) 同样地,为了让你更直观地理解这种打包方式,我也给出了打包的示意图。具体如下所示: ![](https://static001.geekbang.org/resource/image/9c/c8/9ceee0961afe0be20a20f663fbf932c8.jpg?wh=1280x454) 这种打包方式适合于单个NALU很小的时候。因此,我们将多个NALU打包到一起也小于1500字节的时候就可以使用。但是由于一般多个视频帧加到一起还小于1500的情况比较少,所以视频数据的RTP打包一般来说用组合封包方式的情况也很少。 3、分片封包方式 分片封包就更复杂一些了,但却是我们经常用到的打包方式。 它是将一个NALU分开打包在连续的多个RTP包中。因此,我们首先需要一个1字节的FU indicator来表示当前RTP包是不是分片封包方式,再用一个1字节的FU Header来表示当前这个RTP包是不是NALU的第一个包,是不是NALU的最后一个包,以及NALU的类型。 为什么需要表示是不是第一个包以及是不是最后一个包呢?这是因为一个NALU被分开放在多个RTP包中,我们需要知道哪个是第一个NALU分片,哪个是最后一个NALU分片,以及哪些是中间分片。这样我们才能组成一个完整的NALU。 那你可能会问,NALU不是已经在NALU Header中有了NALU Type字段吗?为什么FU Header中还要有NALU Type呢?这是因为分片封包时需要去掉NALU Header。因此,我们需要通过FU Header中的NALU Type得到NALU的类型。 其中,分片封装中的FU indicator跟NALU Header的格式也是一样的,也只是Type字段的值不同,所以我们可以参考组合封包小节中的表格。因为我们一般只使用FU-A,所以接下来讲述的将是FU-A的分片封包方式。另外,FU Header格式如下所示: ![](https://static001.geekbang.org/resource/image/d5/63/d50ff5960768966518f9dafdac33a463.jpg?wh=1280x402) 这里我简单解释一下各字段的含义: * S:起始位,占1bit,为1则表示是NALU的第一个RTP包。 * E:结束位,占1bit,为1则表示是NALU的最后一个RTP包。 * R:预留位,占1bit。 * Type:占5bits,表示NALU类型。 分片打包的格式如下: ![](https://static001.geekbang.org/resource/image/e3/bd/e3590b35793907d565293bf592c514bd.jpg?wh=1280x560) 分片打包的示意图如下: ![](https://static001.geekbang.org/resource/image/14/7b/1402a1e69a71d1d9dff7b4f40920907b.jpg?wh=1280x588) 这种打包方式主要用于将NALU数据打包成一个RTP包时大小大于1500字节的时候,这是经常使用的视频RTP打包方法。 好了,以上就是三种打包方式。我们怎么选择使用哪种方式打包呢?一般来说,我们在一个H264码流中会混合使用多种RTP打包方式。一般来说,对于小的P帧、B帧还有SPS、PPS我们可以使用单个NALU封包方式。而对于大的I帧、P帧或B帧,我们使用分片封包方式。当然,你可以根据实际情况进行选择。 ## 小结 好了,以上就是这节课的主要内容。接下来我们来总结一下。 首先,我们一起讨论了RTP协议和RTCP协议的主要作用。RTP协议用来封装音视频数据,并且将音视频数据和一些基本信息打包到RTP包中传输到接收端。而RTCP协议则辅助RTP协议使用,其中一个主要的功能就是用来统计RTP包的发送情况,比如说丢包率和具体哪些RTP包在网络发送的过程中丢失了。RTCP包将这些信息收集起来发送给RTP包的发送端。 然后,我们说明了RTP和RTCP协议是带宽预测和拥塞控制的基础,并且重点强调了RTCP协议本身只统计信息,而带宽预测和拥塞控制算法是需要我们自己实现的,RTCP协议本身并没有这个功能。 最后,我们介绍了H264的RTP打包方式,总共有三种,分别是单NALU封包方式、组合封包方式和分片封包方式。 * 单NALU封包方式,一般适合NALU大小比较小,且打包出来的RTP大小小于1500字节的时候使用。 * 组合封包方式,适合多个NALU都很小,且合并在一起打包的RTP包小于1500字节的时候使用。 * 分片打包,则适合NALU比较大的情况,且打包成一个RTP包其大小会大于1500字节的时候使用。 这几种打包方式不是说只能选择一种,在一个RTP流中是可以存在多种打包方式的,即可以混合使用。 最后再一次强调,这节课和H264码流结构那节课都是非常重要的。它们在实际视频开发的过程中会经常用到,希望你可以熟练掌握。 ## 思考题 为什么我们在选择RTP打包方式的时候,需要根据NALU大小是不是大于1500字节(MTU)来选择? 欢迎你在留言区和我分享你的思考和疑惑,你也可以把今天所学分享给身边的朋友,邀请他加入探讨,共同进步。下节课再见。