前言
最近在做 Android 云手机项目,以前是用传图片的方式显示远程手机的屏幕内容,优点是实现起来简单,缺点是流量有些大。现在要改成更加优化的方案:播放 H264 视频流。(视频流数据来自服务器,通过 Socket 传递。并没有使用流媒体协议)。因此先学习了一下 H264 的相关知识。
基本术语
- SPS:序列参数集(Sequence parameter set)
- PPS:图像参数集(Picture parameter set)
- NAL:网络抽象层(Network Abstract Layer)
- I Slice,P Slice,B Slice
- I Frame,P Frame,B Frame
- IDR Picture
SPS
包含的是针对一连续编码视频序列的参数,如标识符 seq_parameter_set_id
、帧数及 POC 的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等。
两个IDR图像之间为序列参数集。
PPS
对应的是一个序列中某一幅图像或者某几幅图像,其参数如标识符 pic_parameter_set_id
、可选的 seq_parameter_set_id
、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。
序列和图像参数集机制,减少了重复参数的传送,每个VCL NAL单元包含一个标识,指向有关的图像参数集,每个图像参数集包含一个标识,指向有关的序列参数集的内容因此,只用少数的指针信息,引用大量的参数,大大减少每个VCL NAL单元重复传送的信息。
Slice(数据片):组成片的编码数据存放在 3 个独立的 DP(Slice A、B、C)中,各自包含一个编码片的子集。
Slice A 包含片头和片中每个宏块头数据。
Slice B 包含帧内和 SI 片宏块的编码残差数据。
Slice C 包含帧间宏块的编码残差数据。
每个Slice可放在独立的 NAL
单元并独立传输。
编码器将每个NAL
各自独立、完整地放入一个分组,因为分组都有头部,解码器可以方便地检测出NAL
的分界,并依次取出NAL进行解码。
每个NAL
前有一个起始码 0x00 00 01
(或者0x00 00 00 01
)(我这边实际遇到的都是0x00 00 00 01
开头的 ),解码器检测每个起始码,作为一个NAL
的起始标识,当检测到下一个起始码时,当前NAL
结束。
同时H.264规定,当检测到0x00 00 00
时,也可以表征当前NAL
的结束。那么NAL
中数据出现0x00 00 01
或0x00 00 00
时怎么办?H.264引入了防止竞争机制。如果编码器检测到NAL
数据存在0x00 00 01
或0x00 00 00
时,编码器会在最后1个字节前插入一个新的字节0x03
,例如:
0x00 00 00
-> 0x00 00 03 00
0x00 00 01
-> 0x00 00 03 01
0x00 00 02
-> 0x00 00 03 02
0x00 00 03
-> 0x00 00 03 03
解码器检测到0x00 00 03
时,把03
抛弃,恢复原始数据(脱壳操作)。解码器在解码时,首先逐个字节读取NAL
的数据,统计NAL
的长度,然后再开始解码。
I 帧与 IDR 帧
在 H.264 中 I 帧并不具有随机访问的能力,这个功能由 IDR 承担。以前的标准中由 I 帧承担。
IDR 会导致 DPB (Decoded Picture Buffer 参考帧列表。这是关键所在)清空,而 I 不会。
I 和 IDR 帧其实都是I帧,都是使用帧内预测的。但是 IDR 帧的作用是立刻刷新,使错误不致传播,从 IDR 帧开始,重新算一个新的序列开始编码。
IDR 图像一定是 I 图像,但 I 图像不一定是 IDR 图像。一个序列中可以有很多的 I 图像,I 图像之后的图像可以引用 I 图像之间的图像做运动参考。
一个新的 IDR 帧开始,可以重新算一个新的 GOP 开始编码,播放器永远可以从一个 IDR 帧播放,因为在它之后没有任何帧引用之前的帧。如果一个视频中没有 IDR 帧,这个视频是不能随机访问的。所有位于 IDR 帧后的B帧和P帧都不能参考 IDR 帧以前的帧,而普通 I 帧后的 B 帧和 P 帧仍然可以参考I帧之前的其他帧。IDR 帧阻断了误差的积累,而I帧并没有阻断误差的积累。
一个 GOP 序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像,但I帧不一定都是 IDR帧,只有 GOP 序列的第1个I帧是 IDR 帧。
I 帧特点
- I 帧:帧内编码帧 ,I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)
- 它是一个帧内压缩编码帧,压缩比约为7。它将全帧图像信息进行 JPEG 压缩编码及传输;
- 解码时仅用I帧的数据就可重构完整图像;
- I 帧描述了图像背景和运动主体的详情;
- I 帧不需要参考其他画面而生成;
- I 帧是 P 帧和 B 帧的参考帧(其质量直接影响到同组中以后各帧的质量);
- 帧是帧组 GOP 的基础帧(第一帧),在一组中只有一个I帧;
- I 帧不需要考虑运动矢量;
- I 帧所占数据的信息量比较大。
P帧
P 帧:前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或 P 帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面,P帧没有完整画面数据,只有与前一帧的画面差异的数据。P帧的压缩率20。
- P 帧是 I 帧后面相隔1~2帧的编码帧
- P 帧采用运动补偿的方法传送它与前面的 I 或 P 帧的差值及运动矢量(预测误差)
- 解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像
- P 帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧
- P 帧可以是其后面 P 帧的参考帧,也可以是其前后的B帧的参考帧
- 由于P帧是参考帧,它可能造成解码错误的扩散
- 由于是差值传送,P帧的压缩比较高
B帧
B帧:双向预测内插编码帧。B 帧是双向差别帧,也就是 B 帧记录的是本帧与前后帧的差别,要解码 B 帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B 帧压缩率高,约为 50,但是解码时 CPU 会比较累。
- B 帧是由前面的 I 或 P 帧和后面的 P 帧来进行预测的
- B 帧传送的是它与前面的I或P帧和后面的P帧之间的预测误差及运动矢量
- B 帧是双向预测编码帧
- B 帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确
- B 帧不是参考帧,不会造成解码错误的扩散
三种不同的数据形式
SODB 数据比特串 -> 最原始的编码数据
RBSP 原始字节序列载荷 -> 在SODB的后面填加了结尾比特(RBSP trailing bits 一个bit“1”)若干比特“0”,以便字节对齐
EBSP 扩展字节序列载荷 -> 在RBSP基础上填加了仿校验字节(0X03)即刚刚提到的“防止竞争机制”。
原因是:在NALU加到Annexb上时,需要添加每 组NALU之前的开始码StartCodePrefix,如果该NALU对应的slice为一帧的开始则用4位字节表示,ox00000001,否则用3 位字节表示ox000001.为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,就插入一个字节的0x03。解码时将 0x03去掉。也称为脱壳操作。
NAL 单元(NALU)
定义了基于分组和比特流系统的基本格式。
头结构
+————————–+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+–+–+–+–+–+–+–+–+
| F | NRI | Type |
+—————————+
将其转为二进制数据后,解读顺序为从左往右,如下:
(1)第1位 forbidden_zero_bit 禁止位,初始为0。当NAL单元有错误时可设置该值为1,以便接收方纠错或丢掉该单元。
(2)第23位 nal_ref_idc 参考级别(重要性指示)。重要级别,0b11(3)表示非常重要。值越大,越重要。解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。8位 nal_unit_type NAL单元类型
(3)第4
示例1: 0x67(0110 0111)(103)
从左往右4-8位为0 0111,转为十进制7,7对应序列参数集 NALU_TYPE_SPS(序列参数集)(Sequence parameter set)
示例2: 0x68(0110 1000)(104)
从左往右4-8位为0 1000,转为十进制8,8对应序列参数集 NALU_TYPE_PPS(图像参数集)(Picture parameter set)
示例3: 0x65(0110 0101)(101)
从左往右4-8位为0 0101,转为十进制5,5对应 NALU_TYPE_IDR 图像中的片(IDR帧)
示例4: 0x41(0100 0001)(65)
从左往右4-8位为0 0001,转为十进制1,1对应非 IDR 图像中的片
整个 NALU & 0001 1111(0x1F)(31) = 5 即 整个 NALU & 31(十进制) = 5 的是 IDR 帧(也就是NALU Type 等于 5 的,是 IDR 上帧):IdrPicFlag = ( ( nal_unit_type = = 5 ) ? 1 : 0 )
具体NAL单元类型编码表(T-REC-H.264-201906-I!!PDF-E.pdf
第65页),请参见下图:
从这个表我们也可以看出来,IDR 的 I 帧是非常重要的,没有它这个序列的所有帧都无法解码。
序列参数集(SPS)和图像参数集(PPS)也很重要。没有序列参数集(SPS),这个序列的帧就无法解码。没有图像参数集(PPS),那么用到这个图像参数集的帧都无法解码。
顺序要求
H.264/AVC标准对送到解码器的NAL单元顺序是有严格要求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则解码器不能够正确解码。
- 序列参数集 NALU
必须在传送所有以此参数集为参考的其他NAL单元之前传送,不过允许这些NAL单元中间出现重复的序列参数集NAL单元。
所谓重复的详细解释为:序列参数集NAL单元都有其专门的标识,如果两个序列参数集NAL单元的标识相同,就可以认为后一个只不过是前一个的拷贝,而非新的序列参数集。 - 图像参数集NALU
必须在所有以此参数集为参考的其他NAL单元之前传送,不过允许这些NAL单元中间出现重复的图像参数集NAL单元,这一点与上述的序列参数集NAL单元是相同的。 - 不同基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元在顺序上不可以相互交叉,即不允许属于某一基本编码图像的一系列片段(slice)单元和数据划分片段(data partition)单元中忽然出现另一个基本编码图像的片段(slice)单元片段和数据划分片段(data partition)单元。
- 参考图像的影响:如果一幅图像以另一幅图像为参考,则属于前者的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于后者的片段和数据划分片段之后,无论是基本编码图像还是冗余编码图像都必须遵守这个规则。
- 基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于相应冗余编码图像的片段(slice)单元和数据划分片段(data partition)单元之前。
- 如果数据流中出现了连续的无参考基本编码图像,则图像序号小的在前面。
- 如果arbitrary_slice_order_allowed_flag置为1,一个基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元的顺序是任意的,如果arbitrary_slice_order_allowed_flag置为零,则要按照片段中第一个宏块的位置来确定片段的顺序,若使用数据划分,则A类数据划分片段在B类数据划分片段之前,B类数据划分片段在C类数据划分片段之前,而且对应不同片段的数据划分片段不能相互交叉,也不能与没有数据划分的片段相互交叉。
- 如果存在SEI(补充增强信息)单元的话,它必须在它所对应的基本编码图像的片段(slice)单元和数据划分片段(data partition)单元之前,并同时必须紧接在上一个基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元后边。假如SEI属于多个基本编码图像,其顺序仅以第一个基本编码图像为参照。
- 如果存在图像分割符的话,它必须在所有SEI 单元、基本编码图像的所有片段slice)单元和数据划分片段(data partition)单元之前,并且紧接着上一个基本编码图像那些NAL单元。
- 如果存在序列结束符,且序列结束符后还有图像,则该图像必须是IDR(即时解码器刷新)图像。序列结束符的位置应当在属于这个IDR图像的分割符、SEI 单元等数据之前,且紧接着前面那些图像的NAL单元。如果序列结束符后没有图像了,那么它的就在比特流中所有图像数据之后。
- 流结束符在比特流中的最后。
H264有两种封装:
一种是 annexb 模式,传统模式,有startcode,SPS 和 PPS是在 ES 中。
一种是 mp4 模式,一般 mp4 mkv 会有,没有startcode,SPS 和 PPS 以及其它信息被封装在 container 中,每一个 frame 前面是这个 frame 的长度。
很多解码器只支持 annexb 这种模式,因此需要将 mp4 做转换:
在 ffmpeg 中用 h264_mp4toannexb_filter 可以做转换。
关于 Profile
具体含义及详细说明,详见T-REC-H.264-201906-I!!PDF-E.pdf
第285页。
以下是简要说明:
profile_idc的u(8) 表示 Profile 类型。
说明:以下 profile_idc
值均为十进制。
- 66 Baseline 基本画质(只有 I 片(Slice)和 P 片(Slice),并且
nal_unit_type
不应该等于2,3,4)
有一种特殊的Baseline Profile
叫Constrained Baseline
,详见 PDF 文档,第285页。 - 77 Main 主流画质(只有 I 片(Slice),P 片(Slice)和 B 片(Slice),并且
nal_unit_type
不应该等于2,3,4) - 88 Extended 进阶画质
- 100 High 高级画质(只有 I 片(Slice),P 片(Slice)和 B 片(Slice),并且
nal_unit_type
不应该等于2,3,4)
有一种特殊的High Profile
叫Progressive High profile
(profile_idc
为 100,并且constraint_set4_flag
等于 1)详见 PDF 文档,第287页。
还有一种High Profile
叫Constrained High profile
(profile_idc
为 100,并且constraint_set4_flag
和constraint_set5_flag
都等于 1)详见 PDF 文档,第288页。 - 110 High 10(只有 I 片(Slice),P 片(Slice)和 B 片(Slice),并且
nal_unit_type
不应该等于2,3,4)
有一种特殊的High 10 Profile
叫Progressive High 10 profile
(profile_idc
为 110,并且constraint_set4_flag
等于 1),详见 PDF 文档,第288页。 - 122 High 4:2:2(只有 I 片(Slice)和 P 片(Slice),并且
nal_unit_type
不应该等于2,3,4) - 244 High 4:4:4 Predictive(只有 I 片(Slice)和 P 片(Slice),并且
nal_unit_type
不应该等于2,3,4) - High 10 Intra(满足
High 10 profil
的所有限制条件,所有图像都是 IDR 图像)(profile_idc
等 110, 并且constraint_set3_flag
等于 1) - High 4:2:2 Intra(满足
High 4:2:2 profile
的所有限制条件,所有图像都是 IDR 图像)(profile_idc
等 122, 并且constraint_set3_flag
等于 1) - High 4:4:4 Intra(满足
High 4:4:4 profile
的所有限制条件,所有图像都是 IDR 图像)(profile_idc
等 244, 并且constraint_set3_flag
等于 1) - 44 CAVLC 4:4:4 Intra(满足
High 4:4:4 Intra profile
的所有限制条件)
说明:
另外,每种 Profile 对解码器都有相应的处理要求,具体详见 PDF 文档。
H264 码率控制
- VBR:Variable BitRate,动态比特率,其码率可以随着图像的复杂程度的不同而变化,因此其编码效率比较高,Motion发生时,马赛克很少。码率控制算法根据图像内容确定使用的比特率,图像内容比较简单则分配较少的码率(似乎码字更合适),图像内容复杂则分配较多的码字,这样既保证了质量,又兼顾带宽限制。这种算法优先考虑图像质量。
- ABR:Average BitRate,平均比特率 是VBR的一种插值参数。ABR在指定的文件大小内,以每50帧 (30帧约1秒)为一段,低频和不敏感频率使用相对低的流量,高频和大动态表现时使用高流量,可以做为VBR和CBR的一种折衷选择。
- CBR:Constant BitRate,是以恒定比特率方式进行编码,有Motion发生时,由于码率恒定,只能通过增大QP来减少码字大小,图像质量变差,当场景静止时,图像质量又变好,因此图像质量不稳定。优点是压缩速度快,缺点是每秒流量都相同容易导致空间浪费。
- CVBR:Constrained Variable it Rate,VBR的一种改进,兼顾了CBR和VBR的优点:在图像内容静止时,节省带宽,有Motion发生时,利用前期节省的带宽来尽可能的提高图像质量,达到同时兼顾带宽和图像质量的目的。这种方法通常会让用户输入最大码率和最小码率,静止时,码率稳定在最小码率,运动时,码率大于最小码率,但是又不超过最大码率。
参考文献
- https://blog.csdn.net/tantion/article/details/82703228
- https://blog.csdn.net/mincheat/article/details/48713047
- https://blog.csdn.net/wh8_2011/article/details/51887762
- https://blog.csdn.net/DaveBobo/article/details/75041348
- https://blog.csdn.net/yu540135101/article/details/84754432
- https://blog.csdn.net/zhaoyun_zzz/article/details/87302600
- https://www.jianshu.com/p/e607e63fb78f
- https://www.jianshu.com/p/560ec8dd7535