音视频传输:RTP协议详解和H.264打包方案

RTP协议背景:

RTP协议即Real-time Transport Protocol是一种网络传输协议,一般负责音视频数据的封包和传输。

跟RTSP、RTCP的关系:

RTCP协议:实时传输控制协议即Real-time Transport Control Protocol,这个协议是RTP协议的姊妹协议,它是为了进行服务质量的监视与反馈、媒体间的同步,以及多播组中成员的标识,目前WecRTC用这个协议进行流量和拥塞控制。

RTSP协议:实时流协议即Real Time Streaming Protocol,这是一种会话管理和媒体控制的协议,用的最多地方就是视频监控。视频监控中摄像机、NVR、前后端之间都是用这个协议和RTP协议配合进行流媒体传输。

站在播放器实现角度,理解三者关系,看图:

我用一个开源软件通过RTSP的URL拉取了一个大华摄像头的视频。其中大家看到这些开始播放,暂停、快播这些播放按钮背后的功能就需要靠RTSP来支持。看到这个图像视频数据,其实靠的是RTP协议传输过来的。RTCP呢?由于RTP协议大多数情况是不可靠协议,它只管传输音视频数据,但是并不保证这些数据丢了怎么处理,发快了怎么处理,这种数据的可靠性控制需要的RTCP协议来保障的。不过我们今天只讲RTP协议,通过这个讲解相信你能有个感官认识。

站在网络层次模型的角度,同样看图:

其中RTSP协议就和HTTP协议一样,属于应用层协议,一般传输层靠的是TCP传输。其实本身的语法和HTTP协议都非常相似,后面文章会详细讲解。

RTP协议既可以理解为传输层也可以理解为应用层,这么说是因为RTP负载可以放到RTSP上进行传输,通过二元交织通道方式实现。也可以下层放到TCP进行承载,不过大多数情况RTP的负载协议是UDP,如果放到TCP上大大降低了RTP协议的实时性。之所以设计RTP协议,就是因为为了规避TCP协议的一些缺点,因为TCP协议在操作系统的协议栈上实现了流量和拥塞控制等机制,但是TCP并没有考虑传输视频的情况,它是针对传输任何数据更通用的做法,但是结合流媒体传输的特点,我们发现UDP更适合传输,所以我们把保证传输速度即快又正确这件事交给了上层的RTP和RTCP协议,这样我们开发者是可以来实现和控制这两种协议的,这样就为解决音视频低延时等场景提供了可能。

给个实例看下,上图:

扫描二维码关注公众号,回复: 13801706 查看本文章

RTP协议原理:

1.发送地址的确定:

上面说了RTP协议是发送端传输流媒体数据的,但是往那个IP和端口传输,如何将自己传输的音视频属性告诉给接收端就需要一种机制来实现,常见的做法就是用SDP进行描述,然后通过RTSP、SIP或者HTTP等协议和接收端协商。一般在协商过程中,会确定发送端RTP和RTCP的目的地址,目的地址由一个IP地址和端口对组成,偶数端口就是RTP媒体流的目的端口,偶数端口+1就是RTCP协议的目的端口,其中RTSP协议传输端口的确定就是通过SetUP方法,看下图:

2.RTP数据包的生成:

通过RTSP等协议的SDP信息协商好了RTP数据包的发送目的和传输方式,我们就需要把音视频数据打包成RTP包,用UDP发送给接收端了。RTP不仅可以用来传视频,也可以传音频,甚至可以传输图像和非音视频数据。传输视频不仅可以传输H264编码的数据,也可以传输H265,同样可以传输谷歌的VP8 VP9系列编码的视频裸数据。音频可以传输G7xx系列、AAC系列。那封装好的数据可以传输吗,也是可以的。其中安防中常说的国标流就是RTP+PS形式,也可以传输RTP+TS数据;

 私信我,领取2022最新最全学习提升资料,内容包括(C/C++LinuxFFmpeg webRTC rtmp hls rtsp ffplay srs

3. RTP的灵活性:

之所以看到RTP协议应用场景广,传输的数据格式多,主要是因为RTP协议设计简单,有时少即是多。RTP协议把很多控制权交给了上层应用者,许多字段也是允许用户自己协商和确定,这样RTP协议的生命力和适应性就强很多。下面分析RTP格式和通过一个示例来看下RTP数据包格式。

RTP数据包格式:

RTP固定头:

RTP的数据包由RTP Header + RTP Playload组成。其中RTP固定头如下图所示:

各个字段的解释:

1. V:当前的协议的版本号是2,其中0和1已经在草案规范中被占用,这里基本就是固定值了;

2. P:填充标记,包的末尾包含了一个或者多个填充字节,其中填充字节的第一字节包括了后面填充字节的长度,该长度字段包含自己,主要是为了一些对齐处理;

3. X位,如果为1则说明有扩展头,一般默认为0,很少有场景会用到;

4. CC位:是为了计算后面有多少个CSRC,四位说明则最大支持15个CSRC,一般默认为0。

5. M位:特别对于视频而言就是一帧的结束,视频帧比较大,需要通过多个NALU来传输,当看到M位为1时就认为是这个I帧的结束,由于音频帧比较小,一个RTP包就是一个音频帧,所以该位直接置1。

6. Sequence number序列号:16位,用于标识发送者发送的RTP报文序列号,每发送一个RTP包,则这里就增加1,当达到最大值后,则重新从0开始。刚才说了一般RTP协议是承载协议是UDP,UDP是不可靠传输协议。那我们如何保证接收端收的数据是正确的呢,就是通过这个字段进行重新排序,所以接收端一般收到RTP数据第一件事就是排序。

特别注意两点:

a. 这个序列号的初始值可以为0但是也可以为其它随机值,只要符合+1就行;

b. b.发送端的音频和视频都是通过RTP传输的,但是他们是分别计数的,所用的序列号是不同的。

7. timestamp时间戳:占32位四字节,这个单位要注意是采样率到倒数,不是真实的时间,一般要根据采样率进行换算。这里反应的RTP报文第一个八位组的采样时刻,目的是为了接收端计算延迟、抖动和音视频同步。需要说明的是,一个视频帧的时间戳是相同的,但是一个视频帧数据量很大可能需要多个RTP包传输,这样就存在多个RTP包时间戳相同的情况,音频帧数据小,不存在音频帧跨RTP的情况,所以不存在这个问题。

8. SSR同步信号源:占32位四字节,用于标识同步信号源,这个值只要保证在一路音视频会话里面值不相同即可。该标识符是随机选取的 RFC1889推荐了MD5随机算法。该值的作用就是在会话中标识RTP负载流的身份,给一个唯一标记值。

9. CSRC特约信号源CSRC:同样是32位,四字节。一个RTP头最多可以含有0-15个,如果是1对1的流媒体传输,这个字段就不用处理,直接忽略该字段。但是混流和混音时,则需要把各方的RTP同步信号源列出来,这样接收端就能正确指出交谈双方的身份。

RTP扩展头解析:

RTP提供了扩展机制以实现个性化:某些新的负载格式独立的功能要求的附加信息可以允许放到RTP数据包头的扩展部分进行传输,基本的RT并不定义任何扩展头本身。

我的理解就是为了给RTP传输协议增加一些扩展性,防止未来一些新功能的加入,同时允许用户增加一些私有信息和私有功能在里面,大部分音视频场景都没有启用RTP扩展部分,但是也有例外。在WebRTC中看到利用RTP扩展部分做了FEC(前向纠错,核心思想就是一些异或运算)的算法处理,这样当发生RTP丢包可以通过扩展快速恢复丢包,在网络不好的时候特别有用。如果你对这部分很了解,可以微信后台私信我一起探讨下。

扩展头格式:

字段说明:

扩展字段定义define by profile:16bit两字节,这个由上层的具体实现协议来决定;

扩展头长度length:表示扩展头的长度字段,16bit即2字节,最大扩展长度1024字节;

注意:

如果要启用扩展头,固定头的扩展标记X置1,负载类型playload需要按照规范定义,扩展头字段的长度可以为0,因为不包括头字段的4字节,最大1024.

H264打包RTP的方法:

上面已经交代了,RTP的特点不仅仅支持承载在UDP上,这样利于低延迟音视频数据的传输,另外一个特点是它允许通过其它协议接收端和发送端协商音视频数据的封装和编解码格式,这样固定头的playload type字段就比较灵活。

今天我以H264裸码流NALU为例,给大家讲述下如何进行H264的打包,这也是我上面几篇封装格式讲解的固定套路,其中H264打包的详细方法要参考RFC6184文档。

H.264标准协议定义了两种不同的类型:一种是VCL即Video Coding Layer,一种是NAL即Network Abstraction Layer。其中前者就是编码器吐出来的原始编码数据,没有考虑传输和存储问题。后面这种就是为了展现H.264的网络亲和性,对VCL输出的slice片数据进行了封装为NALUs(NAL Units),然后再封装为RTP包进行传输,这些都是H.264的基础,见后续文章。

NALU的基本格式是:NALU Header + NALU Data,其中NALU的头由一个字节组成如下所示:

+-----------------+

|0|1|2 |3|4|5|6|7|

+-+-+---+-+-+-+

|F| NRI |Type|

+-----------------+

我们看到1-11就是NALU的单个包类型,但是一个NALU的大小是不一样的,如果是非视频数据的SPS PPS才十几个字节,对于IDR帧,则有可能几十KB。这样把NALU打包到RTP方式就很多:分为一个RTP包承载一个NALU,多个NALU合并到一个RTP,一个大的NALU切分成多个RTP。同时由于时间戳的问题,就有了24-29几种类型。

但是对于发送端组RTP包的一方来说,尽可能找简单的打包方式。对于接受端则需要适配各种发送端的打包方式,因为无法决定输入源的打包方式。这里先分享下我们的打包方式,比较简单:

1. 我们对于NALU的长度<1400的则采用的是单一NALU打包到单一的RTP包中;

2. 我们对于NALU的长度>=1400的则采用了FU-A的方式进行了打包,这种就是把一个大的NALU进行了切分,最后接收方则进行了合并,把多个RTP包合并成一个完整的NALU即可;

3. 至于为什么NALU的长度大于1400字节就要进行FU-A切片,是因为底层MTU大小值固定为1500,从传输效率讲,这里用1400作为切分条件。

同时我们发现现在视频监控领域摄像头通过RTP 传输码流的打包方式都是基本这种,这种打包方案简单容易实现,又满足需要。如下图所示:

① 28、29、30三个RTP分别传输的SPS、PPS、SEI这三种NALU,其中一个NALU分到一个RTP包,这是单一打包方式;

② 31、32三个RTP包分别传输了IDR帧的NALU单元,由于比较大,发送方采用了FU-A的打包方式;

③ 上图还显示了SPS、PPS、SEI的RTP包固定头,Seq初始值不为0,为随机值,并且一个RTP包就顺序+1,这跟上面分析的一致;

④ 上面SPS、PPS、SEI本身不涉及时间戳,但是这里头填充为和自己后面的第一个RTP保持一致,同样IDR的NALU切分的不同RTP包时间戳也是一样的;

那么到底将单一的NALU打包到RTP或者把比较大的NLAU打包到多个RTP即FU-A方式是怎么操作的呢?

打包方式之Single NAL Unit:

就是一个RTP包打包一个单独的NALU方式,其实最好理解,就是在RTP固定头后面直接填充NLAU单元数据即可,即:

RTP Header + NALU Header + NALU Data;

我了验证猜想我进行了抓包和写文件,我们发现写文件的SPS和抓包RTP包固定头后面的负载完全是一致的,写文件中的SPS:

抓包中的RTP固定头后面的SPS:

打包方式之FU-A:

这种打包方式也不复杂,为了解释清楚,需要了解下面两个数据包头即FU indicator和Fu header。

FU indication:

+---------------+

|0|1|2|3|4|5|6|7|

+-+-+-+-+-+-+-+-+

|F|NRI| Type |

+---------------+

这里面的的F和NRI已经在NALU的Header解释清楚了,就是NALU头的前面三个bit位,后面的TYPE就是NALU的FU-A类型28,这样在RTP固定头后面第一字节的后面5bit提取出来就确认了该RTP包承载的不是一个完整的NALU,是其一部分。

那么问题来了,一个NALU切分成多个RTP包传输,那么到底从哪儿开始哪儿结束呢?可能有人说RTP包固定头不是有mark标记么,注意区分那个是以帧图像的结束标记,这里要确定是NALU结束的标记,其次NALU的类型呢?那么就需要RTP固定12字节后面的Fu Header来进行区分。

FU header

+---------------+

|0|1|2|3|4|5|6|7|

+-+-+-+-+-+-+-+-+

|S|E|R| Type |

+---------------+

字段解释:

S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。

E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节,当跟随的FU荷载不是分片NAL单元的最后分片,结束位设置为0。

也就是说一个NALU切片时,第一个切片的SE是10,然后中间的切片是00,最后一个切片时11。

R: 1 bit

保留位必须设置为0,接收者必须忽略该位。

Type: 5 bits

此处的Type就是NALU头中的Type,取1-23的那个值,表示 NAL单元荷载类型定义,


综上所述:

对于比较大的NLAU进行FU-A切片时,其中NALU的Header字段在RTP打包时划分为两个字节

1、NALU header的前3bit为RTP固定头后面第一个字节FU-indication的前3bit,后面5bit后面跟了FU-A打包这种类型28;

2、NALU header的后面5bit变成了RTP固定头第二字节的后面5bit,其中前3bit标识了分片的开始和结束。

为了验证这种打包方式,我们同样进行了写文件和抓包,对第一个IDR帧的NLAU采取的这种分片进行了研究。

第一个IDR帧的NALU第一个切片:

FU indication

十六机制:0x7C

二进制:0111 1100

FU header

十六进制:0x85

二进制:1000 0101

这里的SE是10,则说明该RTP包承载的NALU的第一个切片。

这样我们提取FU indication字节的前3bit位和Fu header字节后5bit位则为0110 0101即0x65这刚好符合IDR帧的NALU Header定义,后面的b8 00 00 03等二进制就是NALU的DATA字段。

第一个IDR帧的NALU第二个切片:

FU indication

十六机制:0x7C

二进制:0111 1100

FU header

十六进制:0x05

二进制:0000 0101

这里的SE是00,则说明该RTP包承载的NALU的中间切片。

按照同样方法提取,则NALU Header的二进制为0110 0101即0x65,同样说明是IDR帧类型,类似这种的中间切片有很多,从31-56RTP包都是中间切片,直到最后一个NALU的切片。

第一个IDR帧的NALU最后一个切片:

FU indication

十六机制:0x7C

二进制:0111 1100

FU header

十六进制:0x45

二进制:0100 0101

这里的SE是01,则说明该RTP包承载的NALU的最后一个切片。

当然通过抓包你还可以到IDR帧时比较大的,而后面的P帧就相对比较小,因为一个NALU切片4次就完了,同样能看到时间戳的变化值等信息。


我们可以看到发送端一般采用Single NAL Unit和FU-A打包方式就基本可以将H264数据发送到接收端了,对于AAC音频来说,直接将ADTS头部去掉以

字节组成一帧直接塞到RTP即可,打包并不难。

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/124273788