RTP协议解析和H264码流提取
原文链接
一、 h264基础概念
SODB: 数据比特串-->最原始的编码数据
RBSP: 原始字节序列载荷-->在SODB的后面填加了结尾比特(RBSP trailing bits 一个bit“1”)若干比特“0”,以便字节对齐。
EBSP: 扩展字节序列载荷– >在RBSP基础上填加了仿校验字节(0X03)它的原因是: 在NALU加到Annexb上时,需要填加每组NALU之前的开始码 StartCodePrefix,如果该NALU对应的slice为一帧的开始则用4位字节表示,ox00000001,否则用3位字节表示 ox000001.
为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,就插入一个字节的0x03。解码时将0x03去掉。 也称为脱壳操作。
H.264的功能分为两层,视频编码层(VCL)和网络提取层(NAL)
VCL数据即被压缩编码后的视频数据序列。把VCL数据要封装到NAL单元中之后,才可以用来传输或存储。NAL单元格式如下图:
H.264 的编码视频序列包括一系列的NAL 单元,每个NAL 单元包含一个RBSP,见表1。编码片(包括数据分割片IDR 片)和序列RBSP 结束符被定义为VCL NAL 单元,其余为NAL 单元。典型的RBSP 单元序列如图2 所示。每个单元都按独立的NAL 单元传送。单元的信息头(一个字节)定义了RBSP 单元的类型,NAL 单元的其余部分为RBSP 数据。
NAL单元
每个NAL单元是一个一定语法元素的可变长字节字符串,包括包含一个字节的头信息(用来表示数据类型),以及若干整数字节的负荷数据。一个NAL单元可以携带一个编码片、A/B/C型数据分割或一个序列或图像参数集。
NALU 头由一个字节组成, 它的语法如下:
NAL单元按RTP***按序传送。其中,T为负荷数据类型,占5bit;R为重要性指示位,占2个bit;最后的F为禁止位,占1bit。具体如下:
(1)NALU类型位
可以表示NALU的32种不同类型特征,类型1~12是H.264定义的,类型24~31是用于H.264以外的,RTP负荷规范使用这其中的一些值来定义包聚合和分裂,其他值为H.264保留。
(2)重要性指示位
用于在重构过程中标记一个NAL单元的重要性,值越大,越重要。值为0表示这个NAL单元没有用于预测,因此可被解码器抛弃而不会有错误扩散;值高于0表示此NAL单元要用于无漂移重构,且值越高,对此NAL单元丢失的影响越大。
(3)禁止位
编码中默认值为0,当网络识别此单元中存在比特错误时,可将其设为1,以便接收方丢掉该单元,主要 用于适应不同种类的网络环境(比如有线无线相结合的环境)。
264常见的帧头数据为:
00 00 00 01 67 (SPS)
00 00 00 01 68 (PPS)
00 00 00 01 65 ( IDR 帧)
00 00 00 01 61 (P帧)
等等,那么他们代表的意思是什么呢?
上述的67,68,65,61,还有41等,都是该NALU的识别级别。
F:禁止为,0表示正常,1表示错误,一般都是0
NRI:重要级别,11表示非常重要。
TYPE:表示该NALU的类型是什么,
见下表,由此可知7为序列参数集(SPS),8为图像参数集(PPS),5代表I帧。1代表非I帧。
由此可知,61和41其实都是P帧(type值为1),只是重要级别不一样(它们的NRI一个是11BIN,一个是10BIN)
NALU类型是我们判断帧类型的利器,从官方文档中得出如下图:
H264(NAL简介与I帧判断)
我们还是接着看最上面图的码流对应的数据来层层分析,以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,
解读顺序为从左往右算,如下:
(1)第1位禁止位,值为1表示语法出错
(2)第2~3位为参考级别
(3)第4~8为是nal单元类型
例如上面00000001后有67,68以及65
其中0x67的二进制码为:
0110 0111
4-8为00111,转为十进制7,参考第二幅图:7对应序列参数集SPS
其中0x68的二进制码为:
0110 1000
4-8为01000,转为十进制8,参考第二幅图:8对应图像参数集PPS
其中0x65的二进制码为:
011 00101
4-8位为00101,转为十进制5,参考第二幅图:5对应IDR图像中的片(I帧)
所以判断是否为I帧的算法为:
(NALU类型 & 0001 1111) = 5 即 (NALU类型 & 31) = 5
比如0x65 & 31 = 5
二、RTP Header解析
1) V:RTP协议的版本号,占2位,当前协议版本号为2
2) P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
3) X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
4) CC:CSRC计数器,占4位,指示CSRC 标识符的个数
5) M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
6) PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。
7) ***:占16位,用于标识发送者所发送的RTP报文的***,每发送一个报文,***增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,***的初始值是随机的,同时音频包和视频包的sequence是分别记数的。
8) 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
9) 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
10) 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
注:基本的RTP说明并不定义任何头扩展本身,如果遇到X=1,需要特殊处理
使用Wireshark抓一段码流进行分析
其中,
80 是V_P_X_CC
60 是M_PT
00 01 是SequenceNum
00 00 00 00 是Timestamp
39 6e d3 46 是SSRC
把前两字节换成二进制如下
1000 0000 0110 0000
按顺序解释如下:
10 是V;
0 是P;
0 是X;
0000 是CC;
0 是M;
110 0000 是PT;
三、RTP荷载H264码流
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
荷载格式定义三个不同的基本荷载结构,接收者可以通过RTP荷载的第一个字节后5位(如上图参考RFC3984)识别荷载结构。
1) 单个NAL单元包:荷载中只包含一个NAL单元。NAL头类型域等于原始 NAL单元类型,即在范围1到23之间
2) 聚合包:本类型用于聚合多个NAL单元到单个RTP荷载中。本包有四种版本,单时间聚合包类型A (STAP-A),单时间聚合包类型B (STAP-B),多时间聚合包类型(MTAP)16位位移(MTAP16), 多时间聚合包类型(MTAP)24位位移(MTAP24)。赋予STAP-A, STAP-B, MTAP16, MTAP24的NAL单元类型号分别是 24,25, 26, 27
3) 分片单元:用于分片单个NAL单元到多个RTP包。现存两个版本FU-A,FU-B,用NAL单元类型 28,29标识
常用的打包时的分包规则是:如果小于MTU采用单个NAL单元包,如果大于MTU就采用FUs分片方式。因为常用的打包方式就是单个NAL包和FU-A方式,所以我们只解析这两种。
3.1 单个NAL单元包
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a Single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
定义在此的NAL单元包必须只包含一个。这意味聚合包和分片单元不可以用在单个NAL 单元包中。并且RTP序号必须符合NAL单元的解码顺序。NAL单元的第一字节和RTP荷载头第一个字节重合。(如上图参考RFC3984)打包H264码流时,只需在帧前面加上12字节的RTP头即可。
3.2 分片单元(FU-A)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
图 1
分片只定义于单个NAL单元不用于任何聚合包。NAL单元的一个分片由整数个连续NAL单元字节组成。每个NAL单元字节必须正好是该NAL单元一个分片的一部分。相同NAL单元的分片必须使用递增的RTP序号连续顺序发送(第一和最后分片之间没有其他的RTP包)。相似,NAL单元必须按照RTP顺序号的顺序装配。
当一个NAL单元被分片运送在分片单元(FUs)中时,被引用为分片NAL单元。STAPs,MTAPs不可以被分片。 FUs不可以嵌套。 即, 一个FU 不可以包含另一个FU。运送FU的RTP时戳被设置成分片NAL单元的NALU时刻。
图 1表示FU-A的RTP荷载格式。FU-A由1字节的分片单元指示(如图2),1字节的分片单元头(如图3),和分片单元荷载组成。
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
图 2
FU指示字节的类型域Type=28表示FU-A。。NRI域的值必须根据分片NAL单元的NRI域的值设置。
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
图 3
S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。
E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的 FU荷载不是分片NAL单元的最后分片,结束位设置为0。
R: 1 bit 保留位必须设置为0,接收者必须忽略该位
打包时,原始的NAL头的前三位为FU indicator的前三位,原始的NAL头的后五位为FU header的后五位。
使用Wireshark抓一段码流进行分析
前12字节是RTP Header
7c是FU indicator
85是FU Header
FU indicator(0x7C)和FU Header(0x85)换成二进制如下
0111 1100 1000 0101
按顺序解析如下:
0 是F
11 是NRI
11100 是FU Type,这里是28,即FU-A
1 是S,Start,说明是分片的第一包
0 是E,End,如果是分片的最后一包,设置为1,这里不是
0 是R,Remain,保留位,总是0
00101 是NAl Type,这里是5,说明是关键帧(不知道为什么是关键帧请自行谷歌)
打包时,FUindicator的F、NRI是NAL Header中的NRI,Type是28;FU Header的S、E、R分别按照分片起始位置设置,Type是NAL Header中的Type。
解包时,取FU indicator的前三位和FU Header的后五位,即0110 0101(0x65)为NAL类型(即I帧)。
推荐阅读