Little Deamon.

h264结构分析

字数统计: 2.4k阅读时长: 8 min
2019/04/02 Share

h264结构分析

h264码流分层结构的疑惑

在音视频领域,h264可以说是运用非常广泛。网上能找到的关于h264的资料冗杂,最近因为工作原因需要实时推流一段h264裸流。根据测试发现,有些流没有相关的帧率信息,在使用gstreamer向kurento推流的时候出现错乱的问题等。当本地使用ffmpeg重新编码后再推流则正常。

为了debug,借此契机做一下h264的码流结构分析。下图是网上随处可见的码流分层结构。看到这个图肯定会有几个疑问

  1. mediainfo读出的相关信息存在什么地方 直达解答
  2. 帧在这个图里位于哪个层次 太长不看

根据以上问题我们来找一些具体的例子看看

一个简单序列的码流

一个h264裸流是的组成由大到小是:序列、图像(大多时候称为帧,包括I,P,B帧)、片组、片(包括I,P,B片,SP片,SI片)、NALU、宏块、亚宏块、块、像素。

(上图工具https://github.com/latelee/H264BSAnalyzer ,改进于雷神亲笔的开源工具,雷神斯人已逝,精神长存!)

先看第一个字段,长度为28,type为sps。

我们现在打开的是一个NALU单元。一个NALU单元由NALU header和NALU的主体构成。NALU但全的前8位就是NALU header。

  • forbidden_zero_bit 在 H.264 规范中规定了这一位必须为 0。
  • nal_ref_idc,决定了这个NALU的重要性,为0的时候解码器可以考虑丢弃。像此处为3,说明该sps单元为重要性高的单元
  • nal_unit_type,其中,nal_unit_type为1, 2, 3, 4, 5的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元

NAL全称Network Abstract Layer,即网络抽象层。在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。NAL单元是NAL的基本语法结构,它包含一个字节的头信息和一系列来自VCL的称为原始字节序列载荷(RBSP)的字节流。

  • profile_idc level_idc 指明所用 profile、level

下面还有非常多的内容,在很多sps的分析博客中都有写所以我们暂时不谈,重点关注一下我们之前提到的mediainfo没有帧率相关的问题。

补充:H264码流中SPS PPS详解 对sps和pps进行了详细解释


对比了成功推流和推流失败的两个视频的sps后,我们可以发现区别在最后的vui_parameters_present_flag字段。在推流成功的视频的sps中,这个flag为1。然后我们可以在字段中找到num_units_in_tick和time_scale的字段。用这两个做一个除法,即sps->time_scale / sps->num_units_in_tick就能的到我们的答案。

我们最终可以引出

SPS即Sequence Paramater Set,又称作序列参数集。SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS的NAL Unit通常位于整个码流的起始位置。

从整体看这段码流


刚刚那张截图的info中拉到最下是P Slice #188,我们这个视频是189帧。那么188加上第一个I帧正好是189帧。接触过视频技术的人应该都不陌生I帧P帧和B帧。但与h264相关的概念经常提到的是I/P/B 切片(Slice)。根据对这个裸流的分析,我们不难猜测:在这个裸流中一个NALU(NAL 单元)对应的是一帧,每一个单元只有一个切片。

如果不采用FMO(灵活宏块排序) 机制,则一幅图像只有一个片组
如果不使用多个片,则一个片组只有一个片
如果不采用DP(数据分割)机制,则一个片就是一个NALU一个 NALU 也就是一个片

我认为这三条解决和很多人对于帧和NALU关系的疑惑。

对于h264很多人推荐淡化帧的概念,将NALU作为一个基本的单元。在这里如果按照上面的理解,之后就可以不用太纠结帧的问题了。

再看I/IDR,P,B切片

虽然很多博客和视频都提到过这个,但为了内容完整性在这里引用一段曾经看到过的博文

IDR(Instantaneous Decoding Refresh)–即时解码刷新。

I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。

I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR**,这样就方便控制编码和解码流程。 IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。 IDR会导致DPB(DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而I不会。IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图像可以引用I图像之间的图像做运动参考。一个序列中可以有很多的I图像,I图像之后的图象可以引用I图像之间的图像做运动参考。

对于IDR帧来说,在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容,与此相反,对于普通的I-帧来说,位于其之后的B-和P-帧可以引用位于普通I-帧之前的I-帧。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧 。

收到 IDR 帧时,解码器另外需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。

对IDR帧的处理(与I帧的处理相同):(1) 进行帧内预测,决定所采用的帧内预测模式。(2) 像素值减去预测值,得到残差。(3) 对残差进行变换和量化。(4) 变长编码和算术编码。(5) 重构图像并滤波,得到的图像作为其它帧的参考帧。

多参考帧情况下, 举个例子 :有如下帧序列: IPPPP I P PPP ……。按照 3 个参考帧编码。

因为“按照 3 个参考帧编码”,所以参考帧队列长度为 3 。

遇到绿色的 I 时,并不清空参考帧队列,把这个 I 帧加入参考帧队列(当然 I 编码时不用参考帧。)。再检测到红色的 P 帧时,用到的就是 PPI 三帧做参考了。

P帧:前向预测编码帧

在针对连续动态图像编码时,将连续若干幅图像分成P,B,I三种类型,P帧由在它前面的P帧或者I帧预测而来,它比较与它前面的P帧或者I帧之间的相同信息或数据,也即考虑运动的特性进行帧间压缩。P帧法是根据本帧与相邻的前一帧(I帧或P帧)的不同点来压缩本帧数据。采取P帧和I帧联合压缩的方法可达到更高的压缩且无明显的压缩痕迹。

P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找出P帧“某点”预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧某点样值,从而可得到完整的P帧。

有的视频序列比较简单,就没有B帧,

B帧:双向预测内插编码帧

B帧的预测与重构

B帧法是双向预测的帧间压缩算法。当把一帧压缩成B帧时,它根据相邻的前一帧、本帧以及后一帧数据的不同点来压缩本帧,也即仅记录本帧与前后帧的差值。只有采用B帧压缩才能达到200:1的高压缩。

B帧是以前面的I或P帧和后面的P帧为参考帧,找出B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中。

原文出处:https://blog.csdn.net/sphone89/article/details/8086071

自此,前三层应该已经基本清晰,因为涉及到项目的bug似乎只停留到了这个层面。再往下的宏块,子块,ycbcr这些有机会再来补充完整

CATALOG
  1. 1. h264结构分析
    1. 1.1. h264码流分层结构的疑惑
    2. 1.2. 一个简单序列的码流
    3. 1.3. 从整体看这段码流
    4. 1.4. 再看I/IDR,P,B切片