H.264 NAL层解析(0x00000001,编码,打包,NALU)
1.引言
H.264的主要目标:
1.高的视频压缩比
2.良好的网络亲和性
解决方案:
VCL video codinglayer 视频编码层
NAL network abstraction layer 网络提取层
VCL:核心算法引擎,块,宏块及片的语法级别的定义
NAL:片级以上的语法级别(如序列参数集和图像参数集),同时支持以下功能:独立片解码,起始码唯一保证,SEI以及流格式编码数据传送
VCL设计目标:尽可能地独立于网络的情况下进行高效的编解码
NAL设计目标:根据不同的网络把数据打包成相应的格式,将VCL产生的比特字符串适配到各种各样的网络和多元环境中。
NALU头结构:NALU类型(5bit)、重要性指示位(2bit)、禁止位(1bit)。
NALU类型:1~12由H.264使用,24~31由H.264以外的应用使用。
重要性指示:标志该NAL单元用于重建时的重要性,值越大,越重要。
禁止位:网络发现NAL单元有比特错误时可设置该比特为1,以便接收方丢掉该单元。
2.NAL语法语义
NAL层句法:
在编码器输出的码流中,数据的基本单元是句法元素。
句法表征句法元素的组织结构。
语义阐述句法元素的具体含义。
分组都有头部,解码器可以很方便的检测出NAL的分界,依次取出NAL进行解码。
但为了节省码流,H.264没有另外在NAL的头部设立表示起始位置的句法元素。
如果编码数据是存储在介质上的,由于NAL是依次紧密相连的,解码器就无法在数据流中分辨出每个NAL的起始位置和终止位置。
解决方案:在每个NAL前添加起始码:0X000001
在某些类型的介质上,为了寻址的方便,要求数据流在长度上对齐,或某个常数的整数倍。所以在起始码前添加若干字节的0来填充。
检测NAL的开始:
0X000001和0X00000001
我们必须考虑当NAL内部出现了0X000001和0X000000
如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001。
解决方案:为了防止NAL内部出现0x000001的数据,h.264又提出'防止竞争 emulation prevention"机制,在编码完一个NAL时,如果检测出有连续两个0x00字节,就在后面插入一个0x03,则在NAL数据内肯定不会存在NAL起始码0x000001。当解码器在NAL内部检测到0x000003的数据,就把0x03抛弃,恢复原始数据。
0x000000 >>>>>> 0x00000300(结束码)
0x000001 >>>>>> 0x00000301(起始码)
0x000002 >>>>>> 0x00000302(保留)
0x000003 >>>>>> 0x00000303(保证解码器正常工作)
H.264提出了“防止竞争”机制:
0X000000——0X00000300
0X000001——0X00000301
0X000002——0X00000302
0X000003——0X00000303
为此,我们可以知道:
在NAL单元中,下面的三字节序列不应在任何字节对齐的位置出现
0X000000
0X000001
0X000002
Forbidden_zero_bit=0;
Nal_ref_idc:表示NAL的优先级。0~3,取值越大,表示当前NAL越重要,需要优先受到保护。如果当前NAL是属于参考帧的片,或是序列参数集,或是图像参数集这些重要的单位时,本句法元素必需大于0。
Nal_unit_type:当前NAL 单元的类型
标识NAL单元中的RBSP数据类型,其中,nal_unit_type为1, 2, 3, 4, 5的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。
§ 0:未规定
§ 1:非IDR图像中不采用数据划分的片段
§ 2:非IDR图像中A类数据划分片段
§ 3:非IDR图像中B类数据划分片段
§ 4:非IDR图像中C类数据划分片段
§ 5:IDR图像的片段
§ 6:补充增强信息(SEI)
§ 7:序列参数集(SPS)
§ 8:图像参数集(PPS)
§ 9:分割符
§ 10:序列结束符
§ 11:流结束符
§ 12:填充数据
§ 13:序列参数集扩展
§ 14:带前缀的NAL单元
§ 15:子序列参数集
§ 16 –18:保留
§ 19:不采用数据划分的辅助编码图像片段
§ 20:编码片段扩展
§ 21 –23:保留
§ 24 –31:未规定
3.H.264的NAL层处理
结构示意图:
NAL以NALU(NAL unit)为单元来支持编码数据在基于分组交换技术网络中传输。它定义了符合传输层或存储介质要求的数据格式,同时给出头信息,从而提供了视频编码和外部世界的接口。
NALU:定义了可用于基于分组和基于比特流系统的基本格式
RTP封装:只针对基于NAL单元的本地NAL接口。
三种不同的数据形式:
SODB 数据比特串-->最原始的编码数据 (raw)
RBSP 原始字节序列载荷-->在SODB的后面填加了结尾比特(RBSP trailing bits一个bit“1”)若干比特“0”,以便字节对齐
EBSP 扩展字节序列载荷-->在RBSP基础上填加了仿校验字节(0X03)它的原因是: 在NALU加到Annexb上时,需要添加每组NALU之前的开始码StartCodePrefix,如果该NALU对应的slice为一帧的开始则用4位字节表示,0x00000001,否则用3位字节表示0x000001.为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,就插入一个字节的0x03。解码时将0x03去掉。也称为脱壳操作
处理过程:
1.将VCL层输出的SODB封装成nal_unit, Nal_unit是一个通用封装格式,可以适用于有序字节流方式和IP包交换方式。
2.针对不同的传送网络(电路交换|包交换),将nal_unit 封装成针对不同网络的封装格式。
第一步的具体过程:
VCL层输出的比特流SODB(String OfData Bits),到nal_unit之间,经过了以下三步处理:
1.SODB字节对齐处理后封装成RBSP(RawByte Sequence Payload)。
2.为防止RBSP的字节流与有序字节流传送方式下的SCP(start_code_prefix_one_3bytes,0x000001)出现字节竞争情形,循环检测RBSP前三个字节,在出现字节竞争时在第三字节前加入emulation_prevention_three_byte(0x03)
具体方法:
nal_unit( NumBytesInNALunit ) {
forbidden_zero_bit
nal_ref_idc
nal_unit_type
NumBytesInRBSP = 0
for( i = 1; i < NumBytesInNALunit;i++ ) {
if( i + 2 < NumBytesInNALunit&& next_bits( 24 ) = = 0x000003 ) {
rbsp_byte[ NumBytesInRBSP++ ]
rbsp_byte[ NumBytesInRBSP++ ]
i += 2
emulation_prevention_three_byte /*equal to 0x03 */
} else
rbsp_byte[ NumBytesInRBSP++ ]
}
}
3. 防字节竞争处理后的RBSP再加一个字节的header(forbidden_zero_bit+ nal_ref_idc+ nal_unit_type),封装成nal_unit.
第二步的具体过程:
case1:有序字节流的封装
byte_stream_nal_unit( NumBytesInNALunit ) {
while( next_bits( 24 ) != 0x000001 )
zero_byte /* equal to 0x00 */
if( more_data_in_byte_stream( ) ) {
start_code_prefix_one_3bytes /* equal to 0x000001 */nal_unit( NumBytesInNALunit )
}
}
类似H.320和MPEG-2/H.222.0等传输系统,传输NAL作为有序连续字节或比特流,同时要依靠数据本身识别NAL单元边界。在这样的应用系统中,H.264/AVC规范定义了字节流格式,每个NAL单元前面增加3个字节的前缀,即同步字节。在比特流应用中,每个图像需要增加一个附加字节作为边界定位。还有一种可选特性,在字节流中增加附加数据,用做扩充发送数据量,能实现快速边界定位,恢复同步
Case2:IP网络的RTP打包封装
分组打包的规则
(1)额外开销要少,使MTU尺寸在100~64k字节范围都可以;
(2)不用对分组内的数据解码就可以判别该分组的重要性;
(3)载荷规范应当保证不用解码就可识别由于其他的比特丢失而造成的分组不可解码;
(4)支持将NALU分割成多个RTP分组;
(5)支持将多个NALU汇集在一个RTP分组中。
RTP的头标可以是NALU的头标,并可以实现以上的打包规则。
一个RTP分组里放入一个NALU,将NALU(包括同时作为载荷头标的NALU头)放入RTP的载荷中,设置RTP头标值。为了避免IP层对大分组的再一次分割,片分组的大小一般都要小于MTU尺寸。由于包传送的路径不同,解码端要重新对片分组排序,RTP包含的次序信息可以用来解决这一问题。
NALU分割
对于预先已经编码的内容,NALU可能大于MTU尺寸的限制。虽然IP层的分割可以使数据块小于64千字节,但无法在应用层实现保护,从而降低了非等重保护方案的效果。由于UDP数据包小于64千字节,而且一个片的长度对某些应用场合来说太小,所以应用层打包是RTP打包方案的一部分。
新的讨论方案(IETF)应当符合以下特征:
(1)NALU的分块以按RTP次序号升序传输;
(2)能够标记第一个和最后一个NALU分块;
(3)可以检测丢失的分块。
NALU合并
一些NALU如SEI、参数集等非常小,将它们合并在一起有利于减少头标开销。已有两种集合分组:
(1)单一时间集合分组(STAP),按时间戳进行组合;
(2)多时间集合分组(MTAP),不同时间戳也可以组合。
NAL规范视频数据的格式,主要是提供头部信息,以适合各种媒体的传输和存储。NAL支持各种网络,包括:
1.任何使用RTP/IP协议的实时有线和无线Internet 服务
2.作为MP4文件存储和多媒体信息文件服务
3.MPEG-2系统
4.其它网
NAL规定一种通用的格式,既适合面向包传输,也适合流传送。实际上,包传输和流传输的方式是相同的,不同之处是传输前面增加了一个起始码前缀
在类似Internet/RTP面向包传送协议系统中,包结构中包含包边界识别字节,在这种情况下,不需要同步字节。
NAL单元分为VCL和非VCL两种
VCL NAL单元包含视频图像采样信息,
非VCL包含各种有关的附加信息,例如参数集(头部信息,应用到大量的VCL NAL单元)、提高性能的附加信息、定时信息等
参数集:
参数集是很少变化的信息,用于大量VCL NAL单元的解码,分为两种类型:
1.序列参数集,作用于一串连续的视频图像,即视频序列。两个IDR图像之间为序列参数集。IDR和I帧的区别见下面。
2.图像参数集,作用于视频序列中的一个或多个个别的图像序列和图像参数集机制,减少了重复参数的传送,每个VCL NAL单元包含一个标识,指向有关的图像参数集,每个图像参数集包含一个标识,指向有关的序列参数集的内容因此,只用少数的指针信息,引用大量的参数,大大减少每个VCL NAL单元重复传送的信息。
序列和图像参数集可以在发送VCL NAL单元以前发送,并且重复传送,大大提高纠错能力。序列和图像参数集可以在“带内”,也可以用更为可靠的其他“带外”通道传送。
存储单元:
一组指定格式的NAL单元称为存储单元,每个存储单元对应一个图像。每个存储单元包含一组VCL NAL单元,组成一个主编码图像,VCL NAL单元由表示视频图像采样的像条所组成。存储单元前面可以加一个前缀,分界存储单元,附加增强信息(SEI)(如图像定时信息)也可以放在主编码图像的前面。主编码图像后附加的VCL NAL单元,包含同一图像的冗余表示,称为冗余编码图像,当主编码图像数据丢失或损坏时,可用冗余编码图像解码。
编码视频序列:
一个编码视频序列由一串连续的存储单元组成,使用同一序列参数集。每个视频序列可独立解码。编码序列的开始是即时刷新存储单元(IDR)。IDR是一个I帧图像,表示后面的图像不用参考以前的图像。一个NAL单元流可包含一个或更多的编码视频序列。
I帧和IDR帧的区别:
1.在 H.264 中 I 帧并不具有随机访问的能力,这个功能由 IDR 承担。以前的标准中由 I 帧承担。
2.IDR 会导致 DPB (参考帧列表——这是关键所在)清空,而 I 不会。
3.I和IDR帧其实都是I帧,都是使用帧内预测的。但是IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。
4.IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图像可以引用I图像之间的图像做运动参考。
关于h264拆包,按照FU-A方式说明:
1)第一个FU-A包的FU indicator:F应该为当前NALU头的F,而NRI应该为当前NALU头的NRI,Type则等于28,表明它是FU-A包。FU header生成方法:S = 1,E = 0,R = 0,Type则等于NALU头中的Type。
2)后续的N个FU-A包的FU indicator和第一个是完全一样的,如果不是最后一个包,则FU header应该为:S = 0,E= 0,R = 0,Type等于NALU头中的Type。
3)最后一个FU-A包FU header应该为:S = 0,E= 1,R = 0,Type等于NALU头中的Type。
因此总结就是:同一个NALU分包厚的FU indicator头是完全一致的,FU header只有S以及E位有区别,分别标记开始和结束,它们的RTP分包的序列号应该是依次递增的,并且它们的时间戳必须一致,而负载数据为NALU包去掉1个字节的NALU头后对剩余数据的拆分,这点很关键,你可以认为NALU头被拆分成了FU indicator和FU header,所以不再需要1字节的NALU头了。