全新系列文章已更新:
- Android Media Framework - 开篇
- Android Media Framework(一)OpenMAX 框架简介
- Android Media Framework(二)OpenMAX 类型阅读与分析
- Android Media Framework(三)OpenMAX API阅读与分析
- Android Media Framework(四)Non-Tunneled组件的状态转换与buffer分配过程分析
- Android Media Framework(五)Tunnel Mode
- Android Media Framework(六)插件式编程与OMXStore
- Android Media Framework(七)MediaCodecService
- Android Media Framework(八)OMXNodeInstance - Ⅰ
- Android Media Framework(九)OMXNodeInstance - Ⅱ
- Android Media Framework(十)OMXNodeInstance - Ⅲ
- Android Media Framework(十一)OMXNodeInstance - Ⅳ
- Android Media Framework(十二)OMXNodeInstance - Ⅴ
- Android Media Framework(十三)ACodec - Ⅰ
- Android Media Framework(十四)ACodec - Ⅱ
- Android Media Framework(十五)ACodec - Ⅲ
- Android Media Framework(十六)ACodec - Ⅳ
这一篇内容旨在对H.264码流中的一些概念做简单了解。
1、概念了解
VCL
:Video Coding Layer视频编码层,它是H.264(AVC)编码中的核心,负责视频数据的编码工作。VCL层会应用一系列的图像压缩技术,如预测编码、变换编码、量化、熵编码等,将原始视频数据压缩成二进制比特流,压缩生成的比特流也被称为原始数据比特流SODB(String of Data Bits)
。
NAL
:Network Abstraction Layer网络抽象层,将SODB打包封装便于网络传输,里面有如下内容需要做了解:
-
网络传输一般是以字节为单位,但是SODB的长度不一定是整数字节,所以第一步是要把SODB扩充为整数字节,扩充后的数据被称为
RBSP(Raw Byte Sequence Payload)
。扩充方式是在SODB的末尾添加一个比特位1
,如果数据仍不是整数字节,则在末尾添加比特位0,直到数据变成整数字节。-
假设我们原始的SODB是一个7位的二进制数据1001100,将它转化为RBSP时,我们会首先在尾部添加1得到10011001,长度变成8,所以不需要再添加0。
-
如果原始的SODB是一个10位的二进制数据1001100110,将它转化为RBSP时,我们会首先在尾部添加1得到10011001101,长度变成11,需要再添加5个0,最后的RBSP为1001100110100000。
-
-
要将SODB进行封装,形成的独立单元被称为
NALU(Network Abstraction Layer Unit)
。NAL层在每个NALU单元前面都加了Start Code
,Start Code常见的形式有0x000001
和0x00000001
,通过寻找Start Code,很容易就知道NALU在码流中起始和结束位置R了。什么时候用3字节的start code,什么时候用4字节的,我在网络上没有找到明确的说法,在实际解析过程中,只要找3字节的start code就可以了。 -
Start Code的加入会引发一些问题,如果SODB中原本就包含
0x000001
和0x00000001
这两个序列的数据,那么解析NALU起始和结束位置解析就会出现错误了。为了解决这个问题,需要对SODB中的部分字节序列进行处理,添加模拟防止字节(Emulation Prevention Byte)
0x03。具体的做法是,如果检查到连续两个字节是0x0000,如果后面一个字节的值小于等于3(00、01、10、11),那么就在最后一个字节前面添加一个0x03:- 数据中原本是0x000000 -> 0x00000300
- 数据中原本是0x000001 -> 0x00000301
- 数据中原本是0x000002 -> 0x00000302
- 数据中原本是0x000003 -> 0x00000303
- SODB加入模拟防止字节后被称为封装字节序列负载
EBSP(Encapsulate Byte Sequence Payload)
,NALU中封装的负载数据就是它了。
2、NALU
NALU结构如下:
一个NALU由Start Code、NALU Header以及NALU Payload三部分组成,Start Code在前面已经了解过了,所以这里先来看NALU Header。
NALU Header总共一个字节,它包含如下三个字段:
bit 0: forbidden_zero_bit
:这一位始终为0;
bit 1-2: nal_ref_idc
:参考指数,取值范围为0-3,表示这个NALU的重要性,如果等于11,表示是序列参数集,图像参数集,I帧等关键信息帧;如果非11,表示为其他可丢弃帧。
bit 3-7: nal_unit_type
:NALU类型,取值范围为0-31,这里用表格列出值以及对应类型:
nal_unit_type | NALU 类型 |
---|---|
0 | 未使用 |
1 | 编码切片的非IDR图像 Coded slice of a non-IDR picture |
2 | 编码片分区A Coded slice data partition A |
3 | 编码片分区B |
4 | 编码片分区C |
5 | IDR图像 Coded slice of an IDR picture |
6 | SEI补充增强信息 Supplemental enhancement information (SEI) |
7 | 序列参数集 Sequence parameter set |
8 | 图像参数集 Picture parameter set |
9 | 分界符 Access unit delimiter(AUD) |
10 | 序列结束 End of sequence |
11 | 流结束 End of stream |
12 | 填充 Filler data |
13 | 序列参数集扩展 Sequence parameter set extension |
14-18 | 保留 Reserved |
19 | 未分割的辅助图像单元包 Auxiliary slice of a coded picture |
20-23 | 保留 Reserved |
24-31 | 未规定 Unspecified |
接下来介绍几个概念:
-
I帧
:Intra-coded Picture,I帧被称为关键帧,包含了图像全景的所有信息,所以解码不需要依靠其他任何帧; -
P帧
:Predicted Picture,P帧被称为预测帧,P帧表示了前一帧与当前帧之间的差异,前一帧可以是I帧也可以是P帧,用该变量替代图像所有信息可以大大压缩数据; -
B帧
:Bidirectional Predicted Frame,B帧被称为双向预测帧,它的内容通过在其之前和之后的帧(可以是I帧或P帧)上进行插值或运动估计来预测,更进一步提高了压缩效率,但是增加了编码和解码的复杂度; -
IDR帧
:Instantaneous Decoding Refresh Frame,IDR帧被称为即时刷新帧,解码器会丢弃所有历史数据开始新的解码流程,这也是为什么叫即时刷新帧的原因;解码时P帧可能要参考之前的P帧或者I帧,B帧可能要依赖前后帧,IDR帧出现后,后面的帧不会再参考之前的帧;IDR帧解码也是不需要参考其他任何帧的,所以它也属于I帧; -
GOP
:Group of Pictures图像组,指的是由I帧(关键帧)、P帧(预测帧)和B帧(双向预测帧)按一定的顺序组成的视频帧序列,第一个帧通常是I帧,跟随后的帧可以是P帧或者B帧,I帧用于提供全新的画面,P帧和B帧则表示从前一个帧到当前帧的变化;在进行视频播放时,如果中途接收的数据中出现丢失,那么可以通过快速识别GOP结构,丢弃从丢失点开始的整个GOP,然后从下一个I帧(即下一个GOP的开始)重新拉取数据,避免了由于数据丢失造成的视频播放卡顿。
接下来了解以下,如何用代码查找一个完整的NALU:
status_t getNextNALUnit(
const uint8_t **_data, size_t *_size,
const uint8_t **nalStart, size_t *nalSize,
bool startCodeFollows) {
const uint8_t *data = *_data;
size_t size = *_size;
*nalStart = NULL;
*nalSize = 0;
// 1
if (size < 3) {
return -EAGAIN;
}
size_t offset = 0;
// 2
for (; offset + 2 < size; ++offset) {
if (data[offset + 2] == 0x01 && data[offset] == 0x00
&& data[offset + 1] == 0x00) {
break;
}
}
// 3
if (offset + 2 >= size) {
*_data = &data[offset];
*_size = 2;
return -EAGAIN;
}
offset += 3;
size_t startOffset = offset;
for (;;) {
while (offset < size && data[offset] != 0x01) {
++offset;
}
if (offset == size) {
if (startCodeFollows) {
offset = size + 2;
break;
}
return -EAGAIN;
}
// 4
if (data[offset - 1] == 0x00 && data[offset - 2] == 0x00) {
break;
}
++offset;
}
// 5
size_t endOffset = offset - 2;
// 6
while (endOffset > startOffset + 1 && data[endOffset - 1] == 0x00) {
--endOffset;
}
*nalStart = &data[startOffset];
*nalSize = endOffset - startOffset;
if (offset + 2 < size) {
*_data = &data[offset - 2];
*_size = size - offset + 2;
} else {
*_data = NULL;
*_size = 0;
}
return OK;
}
- 检查数据长度是否大于3,一个NALU至少要包含3字节的start code,如果长度小于3那么肯定不包含完整的NALU;
- 查找第一个NALU的start code位置;
- 检查是否找到start code,如果到数据末尾三位都没找到,说明数据中不包含NALU;
- 如果找到start code,那么就要去找下一个NALU的start code;
- 如果找到了下一个NALU的start code,那么此时endOffset位于下一个start code 0x01位置上,此时需要把offset移动到0x00 0x00 0x01序列的第一个0x00上(start code第一位上);
- 第六步的动作有一点要说明,如果NALU的结尾是0x00,那么在下一个start code之前需要添加一个0x00,这个0x00不属于前面一个NALU,所以要跳过添加的0x00这个字节;
- 按照我的理解,只要检查start code前面一个字节是否为0x00就行,如果是endOffset向前移动一位久可以了,不知道这里为什么哟用while来处理。
3、SPS
SPS
:Sequence Parameter Set序列参数集,SPS包含了编解码一系列的参数,包含的信息涉及字段非常多,主要包括以下几类:
- 基本编码参数:比如profile_idc、level_idc等用以指明编码级别和复杂度的参数;
- 视频序列的相关信息:包括编码图像的宽和高、图像比例、帧率等信息;
- 编码结构的参数:诸如用于帧内预测和帧间预测的模式选择、参考帧数、B帧数等参数;
- 熵编码模式:CABAC或CAVLC;
- 量化参数:包括量化矩阵和初始QP等。
SPS的解析需要参考相关spec,这里只贴出android的部分解析代码: