You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

157 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 09RTP & RTCP如何正确地将视频装进RTP中
你好,我是李江。
在前面的课程中我们详细地讲述了视频编码的原理以及预测编码和变换编码的知识。通过这些我们了解了视频编码的基本原理和步骤。同时我们还用了一节课的时间深入探讨了H264的码流结构相信你已经清楚了H264码流是什么样的以及如何从码流中分离出一帧帧图像数据并学会了如何判断这些帧的类型。
那么从这节课开始呢,我们就要进入视频传输和网络对抗部分了。我们会在视频编码码流的基础上,讲讲如何将码流打包成一个个数据包发送到网络上,并进一步讨论如何避免在发送的过程中引起网络拥塞,从而保证视频的流畅性。同时,我们会进一步在后面的课程中讲解如何在网络不断变化的时候做好视频码控算法,如何防止视频出现花屏,以及如何尽量减少视频卡顿等非常有难度的实际工程问题。
这些问题是视频开发过程中经常会遇到且迫切需要解决的重要问题。而解决这些问题的基础就是需要熟悉RTP和RTCP协议也就是我们这节课的重点。
接下来我们会分别从RTP协议、RTCP协议和H264的RTP打包方法这三个方面来展开这节课。首先让我们一起来认识一下RTP协议。
## RTP协议
RTPReal-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协议
RTCPReal-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来选择
欢迎你在留言区和我分享你的思考和疑惑,你也可以把今天所学分享给身边的朋友,邀请他加入探讨,共同进步。下节课再见。