RTP载荷h264码流的解包与封包
一、H264介绍1.1 h264概述h264是一种视频压缩标准。经过压缩后的帧分为:I帧,P帧和B帧:I帧:关键帧,采用帧内压缩技术。(自身可以通过视频解压算法解压成一张单独的完整的图片)P帧:向前参考帧,在压缩时,只参考前面已经处理的帧(只需要参考前面的I帧或P帧)。采用帧音压缩技术。B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧(需要同时参考前面和后面的I帧或P帧)。采用帧间压
1、H264介绍
1.1 h264概述
H264是由ISO和ITU共同提出的继MPEG4之后的新一代数字视频压缩格式,高度压缩数字视频编解码器标准。
经过压缩后的帧分为:I帧,P帧和B帧:
- I帧:关键帧,采用帧内压缩技术。(自身可以通过视频解压算法解压成一张单独的完整的图片)
- P帧:向前参考帧,在压缩时,只参考前面已经处理的帧(只需要参考前面的I帧或P帧),采用帧内压缩技术。
- B帧:双向参考帧,在压缩时,它既参考前面的帧,又参考后面的帧(需要同时参考前面和后面的I帧或P帧),采用帧间压缩技术。
I帧和IDR帧的关系:
IDR帧就是I帧,但是I帧不一定是IDR帧。在一个完整的视频流单元中第一个图像帧是IDR帧,IDR帧是强制刷新帧,在解码过程中,当出现了IDR帧时,要更新sps、pps,原因是防止前面I帧错误,导致sps,pps参考I帧导致无法纠正。
除了I/P/B帧外,还有图像序列GOP:
- GOP:两个I帧之间是一个图像序列,在一个图像序列中只有一个I帧。
- GOP值越大,那么I帧率之间P帧和B帧数量越多,图像画质越精细,如果GOP是120,如果分辨率是720P,帧率是60,那么两I帧的时间就是120/60=2s.
(图出自H.264视频压缩标准白皮书)
在H.264基准类中,仅使用I帧和P帧以实现低延时,因此是网络摄像机和视频编码器的理想选择。
1.2 h264原始码流结构
H264分为两层,VCL(视频编码层)和 NAL(网络提取层)
- VCL:包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码。
- NAL:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别。
VCL数据传输或者存储之前,会被映射到一个NALU中,H264数据包含一个个NALU,如下图:
H.264 的编码视频序列包括一系列的NAL 单元,每个NAL 单元包含一个RBSP。编码片(包括数据分割片IDR 片)和序列RBSP 结束符被定义为VCL NAL 单元,其余为NAL 单元。典型的RBSP 单元序列如图2 所示,每个单元都按独立的NAL 单元传送。单元的信息头(一个字节)定义了RBSP 单元的类型,NAL 单元的其余部分为RBSP 数据。
一个NALU = 一组对应于视频编码的NALU头部信息 + 一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)
NAL单元格式如下图:
一个原始的NALU结构包含以下三部分:
[StartCode] + [NALU Header] + [NALU Payload]
StartCode 是一个NALU单元的开始,必须是00 00 00 01 或者 00 00 01
1.3 NAL单元
每个NAL单元是一个一定语法元素的可变长字节字符串,包括包含一个字节的头信息(用来表示数据类型),以及若干整数字节的负荷数据。一个NAL单元可以携带一个编码片、A/B/C型数据分割或一个序列或图像参数集。
1.3.1 NAL Header
NALU头由一个字节组成,它的语法如下:
NAL单元按RTP序列号按序传送。其中,T为负荷数据类型,占5-bits;R为重要性指示位,占2-bits;最后的F为禁止位,占1bit。具体如下:
- NALU类型位:可以表示NALU的32种不同类型特征,类型1~12是H.264定义的,类型24~31是用于H.264以外的,RTP负荷规范使用这其中的一些值来定义包聚合和分裂,其他值为H.264保留。
- 重要性指示位:用于在重构过程中标记一个NAL单元的重要性,值越大,越重要。值为0表示这个NAL单元没有用于预测,因此可被解码器抛弃而不会有错误扩散;值高于0表示此NAL单元要用于无漂移重构,且值越高,对此NAL单元丢失的影响越大。
- 禁止位:编码中默认值为0,当网络识别此单元中存在比特错误时,可将其设为1,以便接收方丢掉该单元,主要 用于适应不同种类的网络环境(比如有线无线相结合的环境)。
F:禁止位,0表示正常,1表示错误;H264定义此位必须为0
NRI:标识这个NALU单元的重要性(3最高)即:11表示非常重要
TYPE:表示该NALU单元的类型,见下表:
NALU类型是我们判断帧类型的利器,从官方文档中得出如下图:
由图可知:7为序列参数集(SPS),8为图像参数集(PPS),5代表关键帧(I帧)
1.3.2 RBSP序列
下面是RBSP序列的描述
1.4 h264帧判断
H264常见的帧头数据为:
00 00 00 01 61 (P帧)
00 00 00 01 65 ( IDR帧)
00 00 00 01 67 (SPS)
00 00 00 01 68 (PPS)
上述的67、68、65、61,还有41等,都是NALU的识别级别
最上面图的码流对应的数据来层层分析,以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的二进制码为:
0110 0101
4-8位为00101,转为十进制为5,5对应IDR图像中的片(I帧)
所以判断是否为I帧的算法为:
(NALU类型 & 0001 1111)= 5 (即:NALU类型 & 31) = 5 比如0x65 & 31 = 5
即:int value = buf[4] & 0x0f; // 5是I帧,7是sps,8是pps
2、RTP打包发送H264之封包详解
2.1 RTP头结构
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 负载类型 Payload type (PT): 7 bits
- 序列号 Sequence number (SN): 16 bits
- 时间戳 Timestamp: 32 bits
H.264 Payload 格式定义了三种不同的基本的负载(Payload)结构。接收端可能通过 RTP Payload的第一个字节来识别它们。这一个字节类似 NALU 头的格式,而这个头结构的 NAL 单元类型字段则指出了代表的是哪一种结构。
2.2 H264的码流结构
H.264流有两种格式:
- 一种是annexb,也是传统模式,裸流一般都是annexb格式
- annexb格式会在数据包前面加上startcode,然后在后面加上UALU包(NALU Header + RBSP)
- 这个startcode用来做字节流对齐,以及分割流数据。
- 将SPS和PPS都作为一个NALU进行封装,每一次遇到 I 帧之前,都是重复加上SPS和PPS,这个SPS的作用就是提供了序列信息,比如解码信息,而这个PPS提供的是图像信息,比如如何压缩等。
- NALU封装了SPS、PPS、SEI、
- annexb格式的好处,就是解码器可以从任意一个包开始解码。
- 另外种就是AVCC模式,例如mp4、mkv都属于AVCC格式
- 没有startcode,直接是一个个UALU包
- 解码器配置参数在一开始就配置好了,使用NALU长度作为NALU的边界,不需要额外的起始码
- SPS和PPS都封装在文件头部的extradata中;
- 好处就是播放器直接能识别,去除了大量的startcode、sps、pps,缩小了文件大小;
以annexb格式为例:
可能的结构类型分别有:
-
单一 NAL 单元模式
即一个 RTP 包仅由一个完整的 NALU 组成. 这种情况下 RTP NAL 头类型字段和原始的 H.264的
NALU 头类型字段是一样的. -
组合封包模式
即可能是由多个 NAL 单元组成一个 RTP 包. 分别有4种组合方式: STAP-A, STAP-B, MTAP16, MTAP24.
那么这里的类型值分别是 24, 25, 26 以及 27. -
分片封包模式
用于把一个 NALU 单元封装成多个 RTP 包. 存在两种类型 FU-A 和 FU-B. 类型值分别是 28 和 29.
2.2.1 单个 NAL 单元模式
- 一个封装单个NAL单元包到RTP的NAL单元流的RTP序号必须符合NAL单元的解码顺序
- 对于 NALU 的长度小于 MTU 大小的包,一般采用单一 NAL 单元模式
- 对于一个原始的 H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成,其中 [Start Code] 用于标示这是一个NALU 单元的开始,必须是 "00 00 00 01" 或 "00 00 01",NALU 头仅一个字节, 其后都是 NALU 单元内容
- 打包时去除 "00 00 01" 或 "00 00 00 01" 的开始码,把其他数据封包的 RTP包即可
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
例子:
如有一个 H.264 的 NALU 是这样的:[00 00 00 01 67 42 A0 1E 23 56 0E 2F … ]
这是一个序列参数集 NAL 单元。[00 00 00 01] 是4个字节的开始码,67 是 NALU 头,42 开始的数据是 NALU 内容。
封装成 RTP 包将如下:
[ RTP Header ] [ 67 42 A0 1E 23 56 0E 2F ]
即只要去掉 4 个字节的开始码就可以了
2.2.2 组合封包模式
当 NALU 的长度特别小时, 可以把几个 NALU 单元封在一个 RTP 包中。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.3 FU-A的分片封包模式
而当 NALU 的长度超过 MTU 时,就必须对 NALU 单元进行分片封包。也称为 Fragmentation Units (FUs)
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14. RTP payload format for FU-A
1) FU indicator有以下格式:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU指示字节的类型域的28,29表示FU-A和FU-B。NRI域的值必须根据分片NAL单元的NRI域的值设置。(此处Type就是rtp分片类型) 见下表
Type Packet Type name
---------------------------------------------------------
0 undefined -
1-23 NAL unit Single NAL unit packet per H.264
24 STAP-A Single-time aggregation packet
25 STAP-B Single-time aggregation packet
26 MTAP16 Multi-time aggregation packet
27 MTAP24 Multi-time aggregation packet
28 FU-A Fragmentation unit
29 FU-B Fragmentation unit
30-31 undefined
2 ) 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。
R: 1 bit
保留位必须设置为0,接收者必须忽略该位。
Type: 5 bits
此处的Type就是NALU头中的Type,取1-23的那个值,表示 NAL单元荷载类型定义
2.3 解包过程
当接收端接收到一个一个的RTP包时,需要将其中的H264数据取出,并还原原始的H264帧,然后才能交给解码器解码。
单一NALU模式和组合封包模式解包过程其实比较简单,主要看一下分片封包模式:
步骤一:当接收到接收到FU-A类型的RTP Packet时,需要看FU Header中的S / E两位:
- 如果时10,表示时一帧数据的开头部分;
- 如果是00,表示一帧数据的中间部分,有可能多个00的分片;
- 如果是01,表示一帧数据的结尾。
步骤二:把之前所有的数据都暂时保存、拼接成完整的一帧H264数据,然后再插入start code[00 00 00 01],便可以交给解码器解码;
步骤三:还原后的NAL头的八位是由FU indicator的前三位加FU header的后五位组成,即:
nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f)
简略代码如下:
auto nalType = data[0] & 0x1F;
switch(nalType)
{
...
//Fragmentationunit
case NAL_UNIT_TYPE_FU_A:
{
packFlag = data[1] & 0xC0;
switch (packFlag) {
//NAL Unit start packet
case 0x80://一帧的开头
NALUnit[4] = (byte) ((data[0] & 0xE0) | (data[1] & 0x1F));
pktDatacopy(data, 2, NALUnit, 5, length - 2);
tmpLen = length + 5 - 2;
break;
//NAL Unit middle packet
case 0x00:
pktDatacopy(data, 2, NALUnit, tmpLen, length - 2);
tmpLen +=length - 2;
break;
//NAL Unit end packet
case 0x40:
pktDatacopy(data, 2, NALUnit, tmpLen, length - 2);
tmpLen += length - 2;
if (getH264Stream()) {
decodeH264Stream(NALUnit, 0, tmpLen);//开始解码
}
tmpLen =0;
break;
}
}
break;
...
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)