人间观察
因为穷,人会放弃体面: 个人形象的体面,工作的体面,社交的体面,尊严的体面。
在分析H.264码流前,我们得得先获取一个H.264的码流,两种方法获取:一是自己写个代码编码为h264的码流(后续介绍),二是是直接从视频文件里抽取。我们这里采用方法二。当然也有其它方法。
快手抖音的短视频/直播,毫无疑问采取的编码方式肯定是H.264
和AAC
生成的MP4封装格式的视频,我们下载一个mp4(可以看一下文件的简介中的编解码器是否是H.264,AAC),用如下ffmpeg命令抽取h264和aac:
// ffmpeg命令 抽取aac到文件
ffmpeg -i v0200f7b0000bq9dpgfiv42bsnt20920.MP4 -acodec copy -vn 1.aac
// ffmpeg命令 抽取h264到文件
ffmpeg -i v0200f7b0000bq9dpgfiv42bsnt20920.MP4 -c:v copy -bsf:v h264_mp4toannexb -an 1.h264
抽取的h264和aac可以播放吗?当然可以,我用的是mac,mac上可以用vlc播放。
ffmpeg命令可以从官网直接下载可执行的二进制
关于在Android中如何利用clang交叉编译ffmpeg后续文章介绍
H.264码流格式
h264的有两种码流格式:字节流格式和RTP包格式。
字节流格式
Annex-B Byte stream format
,这个是官方h264协议文档中规定的格式,所以它是大多数编码器默认的编码后的输出格式。它的基本数据单位为NAL单元,简称NALU
(Network Abstraction Layer Unit)。每个NALU的前面加上起始码:0x000001
(3个字节)或0x00000001
(4个字节)用于分割,后面会介绍。
RTP包格式
这种格式没有在h264中规定,这种格式不需要起始码分割NALU,而是在NALU开始的几个字节代表NALU的长度。这个我没有过多研究,应该是不常用的。
所以我们这里主要介绍的就是字节流格式的h264裸流。所谓的裸流就是经编码器编码后输出的数据,而没有经过传输协议(比如flv)封装的数据,这样的数据就叫做裸流。
H.264结构
码流分层
如上所说h264码流是由一个接一个的 NALU组成的,但是它按照功能分为
视频编码层:VCL(Video Coding Layer),编码器压缩处理后的压缩视频数据序列。
网络抽象层:NAL(Network Abstraction Layer),负责以网络要求的格式对数据进行打包和传送,是传输层。不管是本地保存还是在网络上传送,都需要通过这一层来传输。
也就是视频编码数据(VCL)在传输或存储(保存到文件)之前,会先被封装进NAL(也就是NALU)单元才可以。
NALU(NAL单元)
h264码流是一系列的NALU组成,用起始码分割每个。所以整体看码流的格式就是:
H264码流 = …Start_Code_Prefix + NALU + Start_Code_Prefix + NALU + …
Start_Code_Prefix
标示的就是起始码,起始码为:0x000001
(3个字节)或0x00000001
(4个字节),起始码中间的部分就是NALU
的部分。
我们看下我们从抖音/快手提取的h264文件的开始部分(因为h264格式开始有SPS,PPS,SEI 分割较多,你可以搜索一下文件后后面的数据流也有):
NALU的主体是:NALU=NALU Header + EBSP
NALU的主体有细分:分别为EBSP、RBSP和SODB。其中EBSP完全等价于NALU主体,而且它们三个的结构关系为:
EBSP包含RBSP,RBSP包含SODB。
EBSP名字叫:扩展字节序列载荷(Encapsulated Byte Sequence Payload)
RBSP名字叫:原始字节序列载荷(Raw Byte Sequence Payload)
SODB(String Of Data Bits)就是最原始的编码数据。
后续介绍,先有个大概的概念区分,真的是概念非常多。
NALU Header
NALU Header 在每个的NALU中,占据一个字节也就是8位。分三部分,如下:
名称 | 占据位数bit | 代表的意义 |
---|---|---|
forbidden_zero_bit |
1bit | h264文档规定,这个值应该为0,当它不为0时,表示网络传输过程中,当前NALU中可能存在错误,解码器可以考虑不对这个NALU进行解码 |
nal_ref_idc |
2bit | 取值0~3,代表当前这个NALU的重要性,取值越大,代表当前NALU越重要 |
nal_unit_type |
5bit | NALU的数据类型,比如是sps,pps,sei,idr等 |
我们主要看一下nal_unit_type
在h264协议中定义如下:
nal_unit_type
=1-5是VCL(视频编码层)单元。
6-代表当前NALU为辅助增强信息(SEI)。一般会埋入视频版权等信息。
7-代表当前NALU为序列参数集SPS,包括一个图像序列的所有信息,即两个 IDR 图像间的所有图像信息,如图像尺寸、视频格式等
8-代表当前NALU为图像参数集PPS,包括一个图像的所有分片的所有相关信息, 包括图像类型、序列号等
一般h264的码流最开始都是SEI,SPS,PPS,IDR(I帧)...,SPS,PPS,IDR(I帧). 一般在IDR(I帧)前有SPS,PPS,也就是每一组图像(GOP序列,图片组)都给予了图像参数集(PPS)和这个序列参数集SPS(SPS)。我们看下最开始提取的抖音的h264文件(也就是上面启始码的后一字节)。
// 这里只贴了关键字节,省略其它的
// 16进制打开,每2位数是一个字节byte=8位(bit)
// 1F的二进制位的后五位为:11111
0000 0001 0605 ffff e1dc 45e9 bde6 d948 SEI 06&1F取该字节的后五位=6
3d31 3a31 2e30 3000 8000 0000 0167 6400 SPS 67&1F取该字节的后五位=7
0303 c0f1 8319 a000 0000 0168 e978 b2c8 PPS 68&1F取该字节的后五位=8
b000 0001 6588 8400 4ffe 841f c0a5 9f35 IDR 65&1F取该字节的后五位=5
71b9 4cd3 13c1 0000 0001 419a 246c 47ff slice(片) 41&1F取该字节的后五位=1
视频的宽高就是在SPS中取出来的。
EBSP和RBSP
NALU的起始码为0x000001
或0x00000001
,但是有一种在NALU的内部也有0x000001
或0x00000001
的数据怎么办?H264采用了一种方法如果NALU内部出现了编码器就在最后一个字节前,插入一个新的字节:0x03。做了如下4种情况的处理:
0x000000 插入x03 0x00000300
0x000001 插入x03 0x00000301
0x000002 插入x03 0x00000302
0x000003 插入x03 0x00000303
0x000003
是为了防止NALU内部本来就有0x000003
这样的数据。
所以说EBSP相较于RBSP,多了防止冲突的一个字节:0x03
。当使用EBSP时,就需要检测EBSP内是否有序列:0x000003
,如果有,则去掉其中的0x03
。这样一来,我们就能得到原始字节序列载荷:RBSP。
我们用提取的抖音的h264文件找下:
3d31 3a31 2e30 3000 8000 0000 0167 6400
1fac d980 b40a 1b01 1000 0003 0010 0000
// 比如67=SPS 的NALU就有一个0303
0303 c0f1 8319 a000 0000 0168 e978 b2c8
b000 0001 6588 8400 4ffe 841f c0a5 9f35
11fe 06cb d3bf 26e6 9d1f ff2c e1b1 aaf2
RBSP和SODB
原始编码数据SODB(String Of Data Bits)他们2个的关系是:
RBSP = SODB + RBSP尾部
RBSP尾部
H264协议文档中有两种尾部表示,如下:
尾部特RBSP语法
RBSP最后一个字节的最后一个比特为rbsp_stop_one_bit
,其值为1,并且当rbsp_stop_one_bit
不是最后一个比特时,用一个或多个rbsp_alignment_zero_bit
,其值为0,补齐以形成一个字节对齐。
条带RBSP尾部
当 nal_unit_type
等于1~5时采用这种尾部。在尾部特RBSP语法的基础上,如果当entropy_coding_mode_flag
值为1,也即当前采用的熵编码为CABAC
,而且more_rbsp_trailing_data
返回为true,也即RBSP中有更多数据时,添加一个或多个0x0000
。
H264的码流结构
所以整体H.264的Annex-B码流格式从概念上来看就是,SODB里就是原始的编码数据。
如有描述不准确欢迎指正。
H.264的协议文档
http://www.itu.int/rec/T-REC-H.264
http://www.itu.int/rec/T-REC-H.264-200503-S/en