音视频流媒体开发-目录
iOS知识点-目录
Android-目录
Flutter-目录
数据结构与算法-目录
uni-pp-目录
1 H264打包RTP的⽅法
RTP的特点不仅仅⽀持承载在UDP上,这样利于低延迟⾳视频数据的传输,另外⼀个特点是它允许通过其它协议接收端和发送端协商⾳视频数据的封装和编解码格式,这样固定头的playload type字段就⽐较灵活。截⽌⽬前为⽌,RTP是我⻅过传输⾳视频数据类型最多的,具体参考:
https://en.wikipedia.org/wiki/RTP_payload_formats。
今天我以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的头由⼀个字节组成如下所示:
参考⽂档:
https://tools.ietf.org/html/rfc3984 过时的
https://tools.ietf.org/html/rfc6184 最新的
我们看到1-11就是NALU的单个包类型,但是⼀个NALU的⼤⼩是不⼀样的,如果是⾮视频数据的SPS PPS才⼗⼏个字节,对于IDR帧,则有可能⼏⼗KB。
这样把NALU打包到RTP⽅式就很多,分为:
- ⼀个RTP包承载⼀个NALU;
- 多个NALU合并到⼀个RTP;
- ⼀个⼤的NALU切分成多个RTP。
同时由于时间戳的问题,就有了24-29⼏种类型。
但是对于发送端组RTP包的⼀⽅来说,尽可能找简单的打包⽅式。对于接受端则需要适配各种发送端的打包⽅式,因为⽆法决定输⼊源的打包⽅式。这⾥先分享下我们的打包⽅式,⽐较简单(打包的时候不要搞太复杂的模式):
- 我们对于NALU的⻓度<=1400(rtp payload size)的则采⽤的是单⼀NALU打包到单⼀的RTP包中;
- 我们对于NALU的⻓度>1400的则采⽤了FU-A的⽅式进⾏了打包,这种就是把⼀个⼤的NALU进⾏了切分,最后接收⽅则进⾏了合并,把多个RTP包合并成⼀个完整的NALU即可;
- 为什么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⽅式是怎么操作的呢?
1.1 打包⽅式之Single NAL Unit
就是⼀个RTP包打包⼀个单独的NALU⽅式,其实最好理解,就是在RTP固定头后⾯直接填充NLAU单元数据即可,即:
RTP Header + NALU Header + NALU Data; (不包括startcode)
为验证猜想,进⾏了抓包和写⽂件,我们发现写⽂件的SPS和抓包RTP包固定头后⾯的负载完全是⼀致的,写⽂件中的SPS:
抓包中的RTP固定头后⾯的SPS:
1.2 打包⽅式之FU-A
这种打包⽅式也不复杂,为了解释清楚,需要了解下⾯两个数据包头即FU indicator和Fu header。
FU indication
这⾥⾯的的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
字段解释:
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头部去掉以1024字节组成⼀帧直接塞到RTP即可,打包并不难。⾄于其他的封装格式如PS、TS或者H265,VPx等数据如何打包RTP,以后再给⼤家进⾏分享,完善这个传输系列。