系列文章目录
ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader
ExoPlayer架构详解与源码分析(9)——TsExtractor
ExoPlayer架构详解与源码分析(10)——H264Reader
ExoPlayer架构详解与源码分析(11)——DataSource
ExoPlayer架构详解与源码分析(12)——Cache
ExoPlayer架构详解与源码分析(13)——TeeDataSource和CacheDataSource
ExoPlayer架构详解与源码分析(14)——ProgressiveMediaPeriod
ExoPlayer架构详解与源码分析(15)——Renderer
ExoPlayer架构详解与源码分析(16)——LoadControl
ExoPlayer架构详解与源码分析(17)——TrackSelector
前言
TsExtractor解封完TS数据后,会根据payload中的视频类型使用指定Reader继续解析,如果payload是H.264格式,就会使用H264Reader来继续解析PES payload部分视频数据流。先上下ProgressiveMediaPeriod的万年老图:
这部分已经可以和SampQueue关联起来了,也就是说图中sampleData的地方就发生在H264Reader中。
H264结构
在看代码前老规矩,先简单了解下H264的码流结构
H264都是由一个个的NAL基本单元组成的,每个NAL由包含一个HEADER和一个DATA,如下图
这些基本的NAL可能为多种类型如上图的SPS,PPS,SLICE,这些类型就定义在NAL的Header之中,Header的结构很简单就一个字节,如下表
名称 | 大小(b) | 说明 |
---|---|---|
forbidden_zero_bit | 1 | 禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误,告诉接收方丢掉该单元,否则为0 |
nal_ref_idc | 2 | 指示当前NALU的优先级,或者说重要性,数值越大表明越重要 |
nal_unit_type | 5 | 表示NALU的类型 |
那么nal_unit_type不同值对应什么类型呢看下表
nal_unit_type | NAL类型 |
---|---|
0 | 未使用 |
1 | 不分区、非 IDR 图像的片 |
2 | SLICE A 片分区 A |
3 | SLICE B 片分区 B |
4 | SLICE C 片分区 C |
5 | IDR 图像中的片 |
6 | Supplemental Enhancement Information(SEI ) 补充增强信息单元 |
7 | Sequence Paramater Set(SPS) 序列参数集 |
8 | Picture Paramater Set(PPS) 图像参数集 |
9 | Access Unit Delimiter(AUD) 分界符 |
10 | End Of Seq 序列结束 |
11 | End Of Stream 码流结束 |
12 | Filler Data 填充 |
13…23 | 保留 |
24…31 | 未使用 |
下面看下几个重要的unitType结构
-
Sequence Paramater Set(SPS) 序列参数集
SPS结构比较复杂这里挑几个用到的名称 大小(b) 说明 profile_idc 8 本视频编码时遵循的profile,profile分为Baseline,Main,Extended等,主要用来规定编码时是否采用某些特性,比如说Baseline profile就规定了只能使用I、P slice进行编码,关于profile的说明可以去查看标准的Annex A。 constraint_set0_flag 1 强制使用Baseline profile进行编码 constraint_set1_flag 1 强制使用Main profile进行编码 constraint_set2_flag 1 强制使用Extended profile进行编码 level_idc 8 本视频遵循的level,level主要规定了每秒最多能处理多少个宏块,最大的帧大小,最大的解码缓存,最大比特率等这些性能相关的东西,如果是硬解码,则比较容易出现由于视频level太高而不能解码的情况。 seq_parameter_set_id ue(v) 本SPS的ID,这个ID主要是给PPS用的 separate_colour_plane_flag 1 separate_colour_plane_flag 等于 1 表示对 4:4:4 色度格式中的三个色彩分量分别进行编码。 如果 separate_colour_plane_flag 的值为 0,则表示不对色彩成分进行单独编码,separate_colour_plane_flag 等于 1 时,主编码图像由三个独立的分量组成,每个分量由一个颜色平面(Y、Cb 或 Cr)的编码采样组成,每个采样使用单色编码语法。在这种情况下,每个色彩平面都与特定的 color_plane_id 值相关联 log2_max_frame_num_minus4 ue(v) 指定了变量 MaxFrameNum 的值,值范围应为 0 至 12(含 12), M a x F r a m e N u m = 2 ( l o g 2 m a x f r a m e n u m m i n u s 4 + 4 ) MaxFrameNum = 2^{(log2maxframenumminus4 +4)} MaxFrameNum=2(log2maxframenumminus4+4) pic_order_cnt_type ue(v) 指定解码图片顺序计数的方法,pic_order_cnt_type 的值范围应为 0 至 2(含 2) pic_width_in_mbs_minus1 ue(v) 图片宽度 pic_height_in_map_units_minus1 ue(v) 图片高度 frame_mbs_only_flag 1 是否只进行帧编码 vui_parameters_present_flag 1 SPS是否包含vui参数, video usability information,在标准的Annex E中有描述,主要包含了视频的比例调整,overscan,视频格式,timing,比特率等信息 aspect_ratio_info_present_flag 1 等于 1 表示存在 aspect_ratio_idc,等于 0 表示不存在 aspect_ratio_idc aspect_ratio_idc 8 指定样本的采样纵横比值。当 aspect_ratio_idc 表示 Extended_SAR(扩展 SAR)时,采样纵横比用 sar_width : sar_height 表示,当没有 aspect_ratio_idc 语法元素时,aspect_ratio_idc 值为 0 sar_width 16 表示样本纵横比的水平尺寸 sar_height 16 表示样本纵横比的垂直尺寸(单位与 sar_width 相同) ue(v)、se(v)表示以哥伦布编码的一种变长压缩算法
-
Picture Paramater Set(PPS) 图像参数集
这里也挑几个用到的讲下名称 大小(b) 说明 pic_parameter_set_id ue(v) 当前PPS的ID,供slice RBSP使用 seq_parameter_set_id ue(v) 当前PPS所属的SPS的ID bottom_field_pic_order_in_frame_present_flag 1 用于POC计算,请参考h.264的POC计算中的bottom_field_flag -
Supplemental Enhancement Information(SEI ) 补充增强信息单元
集成在音视频码流中,用于在音视频内部传递消息,可以保证信息与直播音视频数据的同步,SEI并不是解码过程的必须项,有可能对解码过程(容错、纠错)有帮助,视频传输过程、解封装、解码环节,都可能因为某种原因丢弃SEI ,在视频内容的生成端、传输过程中,都可以插入SEI 信息。插入的信息,和其他视频内容一起经过传输链路到达了消费端。那么在SEI 中可以添加哪些信息呢?传递编码器参数、传递视频版权信息、传递摄像头参数、当然也可以传输字幕信息,后面我们会看到。 -
Slice
视频中的一帧图像可以理解成由一个或多个Slice组成,每一个Slice总体来看都由两部分组成-
Slice header,包含着分片类型、分片中的宏块类型、分片帧的数量以及对应的帧的设置和参数等信息,slice body中的宏块在进行解码时需依赖这些信息
来看下Header 的结构名称 大小(b) 说明 first_mb_in_slice ue(v) 当前slice中包含的第一个宏块在整帧中的位置 slice_type ue(v) 当前slice的类型参照下表 pic_parameter_set_id ue(v) 当前slice所依赖的pps的id;范围 0 到 255 colour_plane_id 2 当标识位separate_colour_plane_flag为true时,colour_plane_id表示当前的颜色分量,0、1、2分别表示Y、U、V分量 frame_num ue(v) 表示当前帧序号,数据长度参考上面的log2_max_frame_num_minus4 field_pic_flag 1 场编码标识位。当该标识位为1时表示当前slice按照场进行编码;该标识位为0时表示当前 slice按照帧进行编码 bottom_field_flag 1 底场标识位。该标志位为1表示当前slice是某一帧的底场;为0表示当前slice为某一帧的顶场 idr_pic_id ue(v) 表示IDR帧的序号。某一个IDR帧所属的所有slice,其idr_pic_id应保持一致。该值的取值范围为[0,65535]。 pic_order_cnt_lsb ue(v) 表示当前帧序号的另一种计量方式 delta_pic_order_cnt_bottom se(v) 表示顶场与底场POC差值的计算方法,不存在则默认为0 delta_pic_order_cnt[0] se(v) 指定编码帧顶部字段的图片顺序计数与预期图片顺序计数的差值 delta_pic_order_cnt[1] se(v) 指定图像顺序计数与编码帧底层字段的预期图像顺序计数的差值 slice_type Name of slice_type 0 P (P slice) 1 B (B slice) 2 I (I slice) 3 SP (SP slice) 4 SI (SI slice) 5 P (P slice) 6 B (B slice) 7 I (I slice) 8 SP (SP slice) 9 SI (SI slice) -
Slice body,通常是一组连续的宏块结构(参照上图),这里就是最终存储像素数据的地方了。宏块中还包含了宏块类型、预测类型、Coded Block Pattern、Quantization Parameter、像素的亮度和色度数据集等等信息。具体结构这个里不是重点不展开。
一个视频由多个帧组成,一帧由多个Slice(片)组成,一个Slice由多个宏块组成,一个宏块又由多个(如4X4)的YUV像素数据组成。
-
看完了这些SPS、PPS、SLICE他们之间关系是怎么样的呢
Slice里的pic_parameter_set_id指向了PPS里的pic_parameter_set_id,而PPS里的seq_parameter_set_id又指向了SPS(序列参数集)里的seq_parameter_set_id,这样一个SPS关联多个PPS,而一个PPS又关联了多个Slice;解码器解码Slice时就通过这些ID查询相关的PPS、SPS获取解码所需的必要信息。
在网络传输流的过程中编码器可能会将每个NAL单元放入到单个独立的网络传输块中,如TS中可能一个包中之包含一个NAL,解码器可以很容易的检测出NAL的分界,然后依次取出NAL来解码,但是实际可能一个包里会包含一个PES头这个头后面跟随了多个NAL单元这种情况该如何找到这些NAL单元的分界呢?
很简单,给NAL前添加0x000001头3个字节,某些情况下会要求NAL长度对齐不足的部分填充0,所以H.264规定当检测到0x000000这3个字节的时候也表示当前NAL结束,这样感觉已经可以解决分界问题了。
但是如果NAL内部数据出现0x000001或者0x000000字段怎么办呢,解码器会误以为这里是新的NAL的开始,导致数据解码出错,于是H.264规定了另一个规则 emulation prevention,在编码器编码完一个NAL时,会再去检测当前NAL中是否包含上述2种字节序列,如果检测出则在最后一个字节前插入一个新字节0x03,当解码器在NAL内部检测到有0x000003 字节序列时,就会把0x03丢弃,恢复数据。
如0x000001 最后一个字节添加0x03 变成0x00000301,解码器丢弃后又变成0x000001。
源码里的ParsableNalUnitBitArray 和NalUnitUtil.unescapeStream方法就是用来丢弃0x03的。
H264Reader
了解了上面的知识,基本就可以开始看代码实现了,这部分最好联系上文ExoPlayer架构详解与源码分析(7)——SampleQueue一起看。
@Override
public void consume(ParsableByteArray data) {
assertTracksCreated();
int offset = data.getPosition();
int limit = data.limit();
byte[] dataArray = data.getData();
// 将当前数据长度计入总长度,此时总数据的尾部和当前数据的尾部就是对齐的
totalBytesWritten += data.bytesLeft();
//到这里已经是解复用后的数据了,将数据发给SampleQueue
output.sampleData(data, data.bytesLeft());
// 循环读取到NAL单元结束
while (true) {
//通过判断是否为0x000001 3字节,确定NAL开始位置,prefixFlags用于保存上一次循环里的3字节信息,防止目标字节被循环分割
int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nalUnitOffset == limit) {
// 读取到最后一个字节,循环结束
nalUnitData(dataArray, offset, limit);
return;
}
// 知道起始位置后,获取第四个字节后5位就是NAL的类型
int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);
//获取NAL开始位置到当前位置的偏移量,当NAL单元开始位置在上一段数据中时,这个值为负值
int lengthToNalUnit = nalUnitOffset - offset;
if (lengthToNalUnit > 0) {
//将当前位置到下一个NAL开始位置的数据输入
nalUnitData(dataArray, offset, nalUnitOffset);
}
//用当前数据的结束位置-相对于当前数据的NAL开始位置,得到就是当前NAL开始位置到当前数据的结束距离
int bytesWrittenPastPosition = limit - nalUnitOffset;
//由于当前的结束位置和整个的结束位置是对齐的,用整个数据的长度减轻到结尾的距离,就是这个NAL相对于整个数据的绝对位置
long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
// 如果到下一个单元开始的长度为负,那么我们向 NAL 缓冲区写入了过多字节。当通知NAL结束时丢弃多余的字节。
endNalUnit(
absolutePosition,
bytesWrittenPastPosition,
lengthToNalUnit < 0 ? -lengthToNalUnit : 0,
pesTimeUs);
// 下个NAL单元开始
startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
// 从NAL单元开始位置读取3个字节
offset = nalUnitOffset + 3;
}
}
//结束NAL单元
private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
if (!