H264入门

前言 为啥要写这个呢?
网上关于h264的文章多如牛毛,但是很多都不全面,不系统,大多深入细节。本着初学的心态,开始只需要一个大致的框架就ok了。具体细节可以自己google。这篇文章就是适合初学者入门的,注重实践吧。我只是知识的搬运工,几乎所有的内容都是参考了各位大神的文章,再次感谢!
OVERVIEW 我们平时遇到的视频文件各式各样,五花八门。通常它们会根据格式的不同,而有着不同的扩展名,比如 avi,rmvb,mkv,mp4 等等等。这些格式代表的都是封装格式。
这些文件通常产生的过程是这样的:

  1. 通过录制工具录制一帧一帧的图像,可能是 Camera,屏幕截取工具等。
  2. 将录制的图像送给编码器进行编码,得到原始的视频码流,也称为裸流。比如视频中常用的 H.264 格式的编码。
  3. 将原始的视频码流封装进 封装格式 文件中,产生我们最终看到的视频文件。
封装格式的文件,由于其通常可以包含更丰富的信息,存储传输方便,而得到广泛的应用,因此可以播放各种各样封装格式文件的播放器也非常多。
而原始的视频码流通常可以为我们学习视频编解码的知识提供极大的方便。由上面封装格式文件产生的过程,可以看出,H.264 原始视频码流的播放,显然要比封装格式文件的播放简单许多,但由于原始码流的实用价值有限,而难以找到相应的播放工具。
实际上,ffmpeg 项目又在原始 H.264 码流播放这个问题上,拯救了广大的视频编解码开发者。
ffmpeg 工具集提供的 ffplay 可以播放 H.264 裸流。直接:
$ ffplay xxx.h264 ffplay version 4.1 Copyright (c) 2003-2018 the FFmpeg developers built with Apple LLVM version 10.0.0 (clang-1000.11.45.5) configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gpl --enable-libmp3lame --enable-libopus --enable-libsnappy --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-opencl --enable-videotoolbox libavutil56. 22.100 / 56. 22.100 libavcodec58. 35.100 / 58. 35.100 libavformat58. 20.100 / 58. 20.100 libavdevice58.5.100 / 58.5.100 libavfilter7. 40.101 /7. 40.101 libavresample4.0.0 /4.0.0 libswscale5.3.100 /5.3.100 libswresample3.3.100 /3.3.100 libpostproc55.3.100 / 55.3.100 [h264 @ 0x7fdbd78a1800] non-existing PPS 0 referenced0B f=0/0 Last message repeated 1 times [h264 @ 0x7fdbd78a1800] decode_slice_header error [h264 @ 0x7fdbd78a1800] no frame! …

就可以播放视频数据了。ffplay 同时打印出了与视频相关的一些信息,如 H.264 的编码配置,图像格式,分辨率等等。
参考:this
H.264简介 MPEG(Moving Picture Experts Group)和VCEG(Video Coding Experts Group)已经联合开发了一个比早期研发的MPEG 和H.263性能更好的视频压缩编码标准,这就是被命名为AVC(Advanced Video Coding),也被称为ITU-T H.264建议和MPEG-4的第10 部分的标准,简称为H.264/AVC或H.264。这个国际标准已经与2003年3月正式被ITU-T所通过并在国际上正式颁布。
H.264是一种对视频数据编解码的一种协议,大家都知道若让视频原始数据包在网络中传输对网络的开销就太大了,想想若是播放超清视频,每帧视频大概就有3MB左右,要让人眼看到流畅的画面,就需要每秒10帧以上,也就是每秒传输就有30MB左右大小。这无疑对网络照成的压力过大。而H.264的最大优势就是具有很高的数据压缩比率,在保证图像的清晰度下,H.264的压缩比是MPEG2的2倍以上,是MPEG4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的10∶1。低码率(LowBitRate)对H.264的高的压缩比起到了重要的作用,和MPEG2和MPEG4ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。
H.264格式
H.264码流分Annex-B和AVCC两种格式。
参考:this
我们先介绍一下 Annex-B 格式。AVCC 和他差不多之后讲。
Annex-B格式也叫MPEG-2 transport stream format格式(ts格式), ElementaryStream格式。
我们先看看在这个格式下h264流到底是个什么样子:
H264入门
文章图片

可以看到,h264的流就是“start code”“NALU” “start code”“NALU” “start code”“NALU”……这两种单元交替往复的结构。Well done,现在你就已经掌握了所有h264的格式需要掌握的要点了。是不是很简单?
嗯。。。
很简单:
  • NALU (Net Abstraction Layer),H264编码数据存储或传输的基本单元,一般H264码流最开始的两个NALU是SPS和PPS,第三个NALU是IDR(SPS,PPS,IDR是啥后面会讲到)。
  • Start code,是用来界定数据包的开始和结尾的单元。他有两种形式,一种是 “00 00 00 01” 四字节,一种是“00 00 01” 三字节。
举个栗子,就拿客户提供的h264裸流文件(后缀.h264)二进制dump一下看看:
H264入门
文章图片

在所截图的这个区域里,一共有4个数据包(NALU)被4个界定符(Start code)分隔开来。这里的Start code用的是“00 00 00 01”这种格式。
好奇的你一定想知道这个NALU包里面到底包含了啥数据,下面我们就简单分析一下:)
NALU的第一个字节称为“NALU indicator”, 这是数据帧的第一个字节,标识该帧类型是什么,结构如下:
H264入门
文章图片

? F: 禁止位,0表示正常,1表示错误,一般都是0
? NRI: 重要级别,11表示非常重要,一般取值为11、10、01
? TYPE: 表示该NALU的类型是什么,是视频数据包呢?还是传输meta data的包呢?还是其他非视频信息的包呢?类型的具体取值可见下表,非常重要:
H264入门
文章图片

撇去其他繁杂的术语名词,就这基本概念而言,确实很简单。。
下面结合一个h264码流文件,分析阐述一下上面的概念,如下图是一个h264文件的二进制流数据:
H264入门
文章图片

从开头开始,start code是”00 00 00 01”; NALU indicator: 0x67 = 0110 0111,所以F = 0,NRI = 11,TYPE = 00111 = 7,所以该NALU类型为序列参数集SPS(Sequence parameter set),然后向后面查找,直到找到下一个NALU的start code为止,光标处就是第二个NALU数据单元开始的地方,可以看到NALU indicator: 0x68 = 0110 1000,所以F = 0,NRI = 11,TYPE = 01000 = 8,所以该NALU类型为图像参数集PPS(Picture parameter set)。
看到这里,你一定会头大,啥是“SPS帧”啥是“PPS帧”,如果你继续分析上面的二进制流文件,接下来的start code后会紧跟着“IDR帧”“non-IDR帧”等等,这些到底是啥?有啥作用?
要弄明白这些,你就必须得了解一些视频压缩方面的基础知识。
参考:this
I帧P帧B帧 视频压缩中,每帧代表一幅静止的图像。而在实际压缩时,会采取各种算法减少数据的容量,其中IPB就是最常见的。简单地说,I帧是关键帧,属于帧内压缩。就是和AVI的压缩是一样的。 P是向前搜索的意思。B是双向搜索。他们都是基于I帧来压缩数据。I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累~。从上面的解释看,我们知道I和P的解码算法比较简单,资源占用也比较少,I只要自己完成就行了,P呢,也只需要解码器把前一个画面缓存一下,遇到P时就使用之前缓存的画面就好了,如果视频流只有I和P,解码器可以不管后面的数据,边读边解码,线性前进,大家很舒服。但网络上的电影很多都采用了B帧,因为B帧记录的是前后帧的差别,比P帧能节约更多的空间,但这样一来,文件小了,解码器就麻烦了,因为在解码时,不仅要用之前缓存的画面,还要知道下一个I或者P的画面(也就是说要预读预解码),而且,B帧不能简单地丢掉,因为B帧其实也包含了画面信息,如果简单丢掉,并用之前的画面简单重复,就会造成画面卡(其实就是丢帧了),并且由于网络上的电影为了节约空间,往往使用相当多的B帧,B帧用的多,对不支持B帧的播放器就造成更大的困扰,画面也就越卡。 一般平均来说,I的压缩率是7(跟JPG差不多),P是20,B可以达到50,可见使用B帧能节省大量空间,节省出来的空间可以用来保存多一些I帧,这样在相同码率下,可以提供更好的画质。

而SPS,PPS帧一般包含了包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile level(后面会讲到),图像的宽和高,deblock滤波器等。一般第一个NALU是SPS帧,第二个NALU是PPS帧。第三个NALU是一个IDR帧(即时解码器刷新)。IDR帧其实就是一个特殊的I帧,因为他是一个GOP中第一个I帧,所以叫IDR帧。
  • SPS(Sequence Parameter Sets):序列参数集,作用于一系列连续的编码图像。对应的type字段值为7。
  • PPS(Picture Parameter Set):图像参数集,作用于编码视频序列中一个或多个独立的图像。对应的type字段值为8。
  • SEI(Supplemental enhancement information):附加增强信息,包含了视频画面定时等信息,一般放在主编码图像数据之前,在某些应用中,它可以被省略掉。对应的type字段值为6。
  • IDR(Instantaneous Decoding Refresh):即时解码刷新。对应的type字段值为5。
这里又引入了一个新的概念——GOP。
一组图像GOP 参考:this
H264入门
文章图片

  • 所谓GOP就是1组图像Group of Picture,在这一组图像中有且只有1个I帧,多个P帧或B帧,两个I帧之间的帧数,就是一个GOP。
  • GOP一般设置为编码器每秒输出的帧数,即每秒帧率,一般为25或30,当然也可设置为其他值。
  • 在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP 开始才有可能得以恢复,所以GOP值也不宜设置过大。
  • 由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。
IDR帧 在一个GOP中的第一个I帧,我们称为“IDR帧” (Instantaneous Decoding Refresh, 即时解码刷新),名字好奇怪,接下来我们看看为啥要取这个名字:
H264中I帧和IDR帧的区别: H.264中规定了两种类型的I帧:普通I帧(normal Iframes)和IDR帧(InstantaneousDecoding Refresh, 即时解码刷新)。 IDR帧实质也是I帧,使用帧内预测。但是功能上比普通I帧更加特殊一些。IDR帧的作用是立即刷新,会导致DPB(Decoded Picture Buffer参考帧列表)清空,而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帧。

理解了IDR帧,我们看看一般的h264的帧流格式:
SPS PPS IDR P B B B B SPS PPS IDR P P P P SPS PPS IDR P B B B B…
如此循环往复。。。
OK,到目前为止,你已经掌握了基本的h264编码相关的知识,其他一些繁杂的术语就不再罗列了,有了这些基础,一看便知!下面,再顺带介绍下H264 profile level的知识。
H264 profile level 参考:this
简单理解就是H264有多个版本(多种画质),版本越高编码效率和压缩率就越高,对应的版本是Profile。
从低到高分别为:Baseline、Main、High
在相同配置情况下,High profile(HP)可以比Main profile(MP)节省10%的码流量,比MPEG-2 MP节省60%的码流量,具有更好的编码性能。
  • Baseline
    支持I/P 帧,只支持无交错(Progressive)和CAVLC
    一般用于低阶或需要额外容错的应用,比如视频通话、手机视频等;
  • Main
    支持I/P/B 帧,无交错(Progressive)和交错(Interlaced),CAVLC 和CABAC
    用于主流消费类电子产品规格如低解码(相对而言)的mp4、便携的视频播放器、PSP和Ipod等;
  • High
    在Main的基础上增加了8x8 内部预测、自定义量化、无损视频编码和更多的YUV 格式(如4:4:4)
    用于广播及视频碟片存储(蓝光影片),高清电视的应用。
一般可以输出H264帧的USB摄像头,使用的是BP-Baseline Profile,只有I帧与P帧。
到底是什么profile level我们可以从SPS帧中查看到。
最后,我们用一个具体例子来分析一段h264的裸视频流,该视频流文件来自客户提供的车载摄像头输出的h264流。
H264码流实例分析 我们可以使用h264码流分析工具,网上有很多,我这里使用的是 H264Naked。
直接加载我们的 app.h264 文件。
H264入门
文章图片

他自动解析了h264裸流文件:左边窗口列出了该文件中按照时间序列的所有NALU单元,包含了 SPS PPS IDR帧 IPB帧 等等。右边的窗口列出了该NALU中的各个字段的信息。
? 第1帧,NALU indicator中显示,nal_unit_type是1,非IDR帧,猜测应该是P帧或者B帧。 (如何区分?)以下的字段都是涉及“帧间预测”算法相关。这里就不展开了。
? 第1-26帧,都是P帧或者B帧。帧大小也都一样。4252字节。
? 第27帧,nal_unit_type是7,是SPS帧,我们对相关字段分析下:
H264入门
文章图片

profile_idc :77
/*标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:
基准档次:baseline profile;
主要档次:main profile;
扩展档次:extended profile;
在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:
profile_idc = 66 → baseline profile;
profile_idc = 77 → main profile;
profile_idc = 88 → extended profile;
所以我们的码流是属于 “main profile”
*/
constraint_set0_flag :0
constraint_set1_flag :1
constraint_set2_flag :0
constraint_set3_flag :0
constraint_set4_flag :0
constraint_set5_flag :0
reserved_zero_2bits :0
level_idc :40
/*
标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。
当前码流中,level_idc = 40,因此码流的级别为4。
/
seq_parameter_set_id :0
chroma_format_idc :1
residual_colour_transform_flag :0
bit_depth_luma_minus8 :0
bit_depth_chroma_minus8 :0
qpprime_y_zero_transform_bypass_flag :0
seq_scaling_matrix_present_flag :0
log2_max_frame_num_minus4 :10
pic_order_cnt_type :0
log2_max_pic_order_cnt_lsb_minus4 :4
delta_pic_order_always_zero_flag :0
offset_for_non_ref_pic :0
offset_for_top_to_bottom_field :0
num_ref_frames_in_pic_order_cnt_cycle :0
num_ref_frames :1
gaps_in_frame_num_value_allowed_flag :0
pic_width_in_mbs_minus1 :79
pic_height_in_map_units_minus1 :44
/

计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:
frame_width = 16 × (pic_width_in_mbs_minus1 + 1);
使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:
PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1;
*/
frame_mbs_only_flag :1
mb_adaptive_frame_field_flag :0
direct_8x8_inference_flag :1
frame_cropping_flag :0
frame_crop_left_offset :0
frame_crop_right_offset :0
frame_crop_top_offset :0
frame_crop_bottom_offset :0
vui_parameters_present_flag :0
? 第28帧,是PPS帧,具体格式不展开了。
? 第29帧,是IDR帧,一个新的GOP开始。清空参考帧列表。
? 第30 – 58帧,参考帧P帧或者B帧。
? 第59帧,又是SPS帧。
? 第60帧,又是PPS帧。
? 第61帧,IDR。
? 。。。循环往复。。。
参考:this
至此,我们对h264裸流的基本分析就结束了。
Annex B格式通常用于实时的流格式,比如说传输流,通过无线传输的广播、网络摄像头在线直播等。在这些格式中通常会周期性的重复SPS和PPS包,经常是在每一个关键帧之前,因此据此建立解码器可以一个随机访问的点,这样就可以加入一个正在进行的流,及播放一个已经在传输的流。
AVCC格式 上面我们详细介绍的是 Annex-B 格式的h264流格式,下面介绍下另外一种格式 AVCC。
两种格式基本类似,但有一些细微的区别:
? Annex-B:使用start code分隔NAL(start code为三字节或四字节,0x000001或0x00000001,一般是四字节);SPS和PPS按流的方式写在头部。
? AVCC:使用NALU长度(固定字节,通常为4字节)分隔NAL;在头部包含extradata(或sequence header)的结构体。
AVCC格式的一个优点是在开始配置解码器的时候可以跳到流的中间播放,这种格式通常用于可以被随机访问的多媒体数据,如存储在硬盘的文件。也因为这个特性,MP4、MKV通常用AVCC格式来存储。
【H264入门】未完待续。。。

    推荐阅读