流媒体编解码流程大致如图1所示:
【流媒体编解码流程 图1】
视频数据编解码层格式包含有:H264,H265,MPEG4等。
本文我们主要对H264编码原理进行整理,并对NALU做简要介绍。
参考来源:H264百度百科
H.264从1999年开始到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准里称为H.264,在MPEG的标准里是MPEG-4的一个组成部分(MPEG-4 Part 10),又叫Advanced Video Codec,因此H.264也常常称为MPEG-4或直接叫AVC。
比如下面使用 MediaInfo工具查看flv音视频文件的信息,可以看到video格式为AVC,其实也就是H264格式。
【flv音视频文件基本信息 图2】
参考来源:H264 编解码协议详解,深入浅出理解视频编码H264结构,h264编解码结构框图
1、H264概述
问题:为什么要对音视频文件进行H264编解码?
因为,在音视频传输过程中,视频文件的传输是个极大的问题;一段分辨率为1920 * 1080,每个像素点为RGB占用3个字节,帧率是25的视频,对于传输带宽的要求是:1920 * 1080 * 3 * 25/1024/1024=148.315MB/s,换成bps则意味着视频每秒带宽为1186.523Mbps,这样的速率对于网络存储是不可接受的。因此视频压缩和编码技术应运而生。
对于视频文件来说,视频由单张图片帧所组成,比如每秒25帧,但是图片帧的像素块之间存在相似性,因此视频帧图像可以进行图像压缩;H264采用了16 * 16的分块大小对,视频帧图像进行相似比较和压缩编码。如下图所示:
【图像切分 图3】
压缩编码可以分为内部压缩和外部压缩。
1)内部压缩
内部压缩指的是一帧图片的内部压缩。当H264对图片进行 16 * 16 分块后,会对每个小块内的图像进行分析,如果2个小块图像比较相近,那么住需要存储一张即可,无需存储重复图块。这样可以有效压缩图片的存储大小。
比如下面一张图片,划分的A、B小块图像分析后是基本一样的,那么只需要存储A即可,B不需要进行存储。
【内部压缩 图4】
2)外部压缩
外部压缩指的是图片间的图像压缩。在每帧图片划分成16 * 16 小块的图像进行分析基础上,比图片间的数据,如果两张图片比较相近,对相同的图像模块只需存储一份,对不同的部分再做存储。避免了重复数据的存储,极大改善了图片压缩空间。
比如下面两张图片 ,除了E小块不同之外,其他都一样,那么存储图1数据后,图2片只需要存储与图片1不同的数据即可。
【外部压缩 图5】
2、H264中的 I帧、P帧和B帧
H264 使用帧内压缩和帧间压缩的方式提高编码压缩率;H264采用了独特的 I帧,P帧和B帧策略来实现,连续帧之间的压缩。
【H264 IBP帧排序 图6】
1)I 帧 (帧内编码帧 intra picture)
I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧表示关键帧,解码时只需要本帧数据就可以完成。I帧可以看成是一个图像经过压缩后的产物。自身可以通过视频解压算法解压成一张单独的完整的图片。
I帧特点:
2)P帧 (前向预测编码帧 predictive-frame)
通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧。
P帧表示这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。
需要参考其前面的一个I frame 或者P frame来生成一张完整的图片。
P帧的预测和重构:
P帧是以 I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加得到P帧“某点”样值,从而得到完整的P帧。
P帧特点:
3)B 帧 (双向预测帧 bi-directional interpolated prediction frame)
既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧。B帧要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片。
B帧是双向差别帧,B帧记录的是本帧与前后帧的差别。要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧的叠加取得最终画面。
B帧的预测和重构:
B帧以前面的 I帧或P帧为参考帧,找出B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运行矢量在两个参考帧中找出预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。
B帧特点:
压缩率比较:B帧 > P 帧 > I 帧
3、H264编码结构解析
H264除了实现对视频的压缩处理外,为了方便网络传输,还提供了对应的视频编码和分片策略。类似网络数据帧封装成IP帧,在H264中将其称为组(GOP,group of picture)、片(slice)、宏块(Macroblock),它们一起组成了H264的码流分层结构。H264将其组织成为序列(GOP)、图片(pictrue)、片(Slice)、宏块(Macroblock)、子块(subblock)五个层次。
【H264结构组织 图7】
H264将视频分为连续的帧进行传输,在连续的帧之间使用 I帧、P帧和B帧。同时对于帧内而言,将图像分块为片、宏块和字块进行分片传输;通过这个过程实现对视频文件的压缩包装。
IDR(Instantaneous Decoding Refresh,即时解码刷新)
一个序列的第一个图像叫做IDR图像(立即刷新图像),IDR图像都是 I 帧图像。(I帧图像不一定是IDR图像)
I 帧和IDR帧都使用帧内预测。I 帧不用参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧。但 IDR不允许这样做。
比如原始图像帧序为: IDR1 B2 B3 P4 B5 B6 P7 B8 B9 I10
解码顺序:
IDR帧的核心作用是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧列清空,将已解码的数据全部输出或抛弃 ,重新查找参数集,开始一个新的序列。这样做的好处是,如果前一个序列出现重大失误,在这里可以获得重新同步的机会。IDR图下之后的图像永远不会使用 IDR帧之前的图像的数据来解码。
下图为一个 H264 码流的示例(从码流帧分析可以看出来B帧不能被当作参考帧)
【H264 码流的示例 图8】
GOP (图像组)主要用作形容一个IDR帧 到下一个IDR帧之间的间隔了多少个帧。
比如说GOP为120,如果是720 p60的话,就是2s一次 I帧。
在码率不变的前提下,GOP值越大,P、B帧的数量越多,平均每个I、P、B帧占用的字节数越多,也更容易获取较好的图像质量。
Reference(参考周期)指两个P帧之间的距离。一个I帧所占用的字节数大于一个P帧,一个P帧所占用的字节数大于一个B帧。Reference越大,B帧的数量越多,同理也更容易获得较好的图像质量。
不过通过提高GOP值来提高图像质量是有限度的,因为:
做直播时,一般不用B帧,因为B帧需要占用较大的缓存,并且容易出现延迟。因为B帧要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片,因此在编码的时候B帧要等到P帧才能发送出去。
比如:收到的I、B、P帧的序列为(后面数值表示收到时间,单位 ms):I0 B40 B80 B120 P160,P帧是160ms的时候才收到, 这样B40帧从收到到发出就会延迟 160-40=120ms。
注意:
1)从上图我们可以知道,一张图片可以有多个NALU。
2)对解码器来说,需要先收到SPS和PPS进行初始化,否则解码器无法解出正常的帧数据。
3)发I帧之前,至少要发送一次SPS和PPS,因此如果在实际应用中遇到H264无法解码的时候,检查SPS和PPS是否有接收到并正常初始化。
NALU结构
H264原始码流(裸流)是由一个接一个NALU组成,功能分为两层:VCL(视频编码层)和NAL(网络提取层):
在VCL进行数据传输或存储之前,这些编码的VCL数据,被映射或封装进NAL单元。
NALU=一组对应视频编码的NALU头部信息+一个原始字节序列负荷(RBSP,RawByte Sequence Payload)
【NALU结构单元的主体结构 图9】
一个原始的H264 NALU单元通常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成,其中Start Code用于表示这是一个NALU单元的开始,必须是“00 00 00 01” 或 “00 00 01”,除此之外基本相当于一个NAL header+RBSP。
对于FFmpeg解复用,MP4,flv等文件读取出来的packet不带Start code,TS文件读取出来的packet带StartCode,因此在对MP4、flv文件解码封装的的时候,需要添加上Startcode,否则会出现生成文件损坏导致无法播放问题。
NALU解析
每个NAL单元是一个一定语法元素的可变长字节字符串,包括包含一个字节的头信息(用来表示数据类型),以及若干整数字节的负荷数据。
NALU头信息(占一个字节大小):
【NALU头信息 图10】
字节位参数说明:
T:负荷数据类型,占 5bit。
nal_unit_type:这个NALU单元的类型,1~12由H.264使用,24~31由H.264以外的应用使用。
R:指示位,占2bit。
nal_ref_idc.:取00~11,似乎指示这个NALU的重要性,如00的NALU解码器可以丢弃它而不影响图像的回放,0~3,取值越大,表示当前NAL越重要,需要优先受到保护。如果当前NAL是属于参考帧的片,或是序列参数集,或是图像参数集这些重要的单位时,本句法元素必需大于0。
F:禁止位,占1bit。
forbidden_zero_bit: 在 H.264 规范中规定了这⼀位必须为 0。
H.264标准指出,当数据流是储存在介质上时,在每个NALU 前添加起始码:0x000001 或 0x00000001,用来指示一个NALU 的起始和终止位置:
例子:
0x00 00 00 01 67 …
0x00 00 00 01 68 …
0x00 00 00 01 65 …
0x67:二进制 0110 0111 ,nal_unit_type:0 0111=7(十进制)
H264有两种封装模式:annexb模式和mp4模式。
很多解码器只支持annexb这种模式,因此需要将mp4做转换:在ffmpeg中用h264_mp4toannexb_filter可以做转换。
转换源码:
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
// 2 初始化过滤器上下文
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);