Android音视频【二】 H264码流结构
因为穷,人会放弃体面: 个人形象的体面,工作的体面,社交的体面,尊严的体面。
在分析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 + EBSPNALU的主体有细分:分别为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_bith264文档规定,这个值应该为0,当它不为0时,表示网络传输过程中,当前NALU中可能存在错误,解码器可以考虑不对这个NALU进行解码nal_ref_idc取值0~3,代表当前这个NALU的重要性,取值越大,代表当前NALU越重要nal_unit_typeNALU的数据类型,比如是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 aaf2RBSP和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的协议文档