H.265视频编码原理总结

Posted Thriller L

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了H.265视频编码原理总结相关的知识,希望对你有一定的参考价值。

H.265视频编码原理总结

转载地址

1 概述

H.265(HEVC High Efficiency Video Coding)是现行H.264标准于2003年实现标准化以来时隔10年推出的新标准,将成为支撑未来十年的影像服务和产品的视频压缩技术。其特点是,支持1080p以上的4K×2K和8K×4K分辨率,将视频压缩率提高至H.264的约2倍。也就是说,能以原来一半的编码速度发送相同画质的视频。例如,按照20Mbit/秒发送的H.264格式视频内容,在相同画质的条件下用HEVC格式只需10Mbit/秒的速度。

1.1 H.265发展背景

H.264虽然是一个划时代的数字视频压缩标准,但是随着数字视频产业链的高速发展,H.264的局限性逐步显现,并且由于H.264标准核心压缩算法的完全固化,并不能够通过调整或扩充来更好地满足当前高清数字视频应用。

视频应用向以下几个方面发展的趋势愈加明显:

  • 高清晰度:数字视频的应用格式从720P向1080P全面升级,在一些视频应用领域甚至出现了4K×2K、8K×4K的数字视频格式;

  • 高帧率:数字视频帧率从30fps向60fps、120fps甚至240fps的应用场景升级;

  • 高压缩率:传输带宽和存储空间一直是视频应用中最关键的资源,因此,在有限的空间和管道中获得最佳的视频体验一直是用户的不懈追求。

    由于数字视频应用在发展中面临上述趋势,如果继续采用H.264编码就会出现如下一些局限性:

  • 宏块个数的爆发式增长,会导致用于编码宏块的预测模式、运动矢量、参考帧索引和量化级等宏块级参数信息所占用的码字过多,用于编码残差部分的码字明显减少。即:单个宏块所表示的图像内容的信息大大减少,导致4×4或8×8块变换后的低频率相似程度也大大提高,会出现大量的冗余;

  • 分辨率的大幅增加,表示同一个运动的运动矢量的幅值将大大增加,H.264中采用一个运动矢量预测值,对运动矢量差编码使用的是哥伦布指数编码,该编码方式的特点是数值越小使用的比特数越少。因此,随着运动矢量幅值的大幅增加,H.264中用来对运动矢量进行预测以及编码的方法压缩率将逐渐降低;

  • 并行度比较低:H.264的一些关键算法,例如采用CAVLC和CABAC两种基于上下文的熵编码方法、deblock滤波等都要求串行编码,并行度比较低。针对GPU、DSP、FPGA、ASIC等这种并行化程序非常的CPU,H.264的这种串行化处理越来越成为制约运算性能的瓶颈。

基于上述视频应用的发展趋势和H.264的局限性,面向更高清晰度、更高频率、更高压缩率的高效视频编码标准H.265应运而生。

HEVC的核心目标:在H.264的基础上,保证相同视频质量的前提下,视频流的码率减少50%。在提高压缩效率的同时,允许编码端适当提高复杂度。

HEVC的编码框架:沿用H.263的混合编码框架,即用帧间和帧内预测编码消除时间域和空间域的相关性,对残差进行变换编码以消除空间相关性,熵编码消除统计上的冗余度。HEVC在混合编码框架内,着力研究新的编码工具或技术,提高视频压缩效率。

HEVC的技术创新:基于大尺度四叉树结构的分割技术,多角度帧内预测技术,运动估计融合技术,高精度运动补偿技术,自适应环路滤波技术以及基于语义的熵编码技术。

1.2 发展历程

早在2004年,ITU-T视频编码专家组(VCEG)开始研究新技术以便创建一个新的视频压缩标准。在2004年10月,H.264/ AVC小组对潜在的各种技术进行了调查。2005年1月VCEG的会议上,VCEG开始指定某些主题为“关键技术”作进一步研究。2005年成立软件代码库称为Key Technical Areas (KTA)用来评估这些新的“关键技术。KTA的软件是在联合模型(JM)基础上由MPEG和VCEG的视频组联合开发的,项目名称暂定为H.265和H.NGVC(Next-generation Video Coding)。

按照NGVC的初步要求,在维持视觉HEVC(High efficiency video coding)。质量相同的情况下,比特率较H.264/MPEG-4 AVC的高中档(high profile),计算复杂度维持在比特率较H.264/MPEG-4 AVC的高中档的1/2至3倍之间。 2009年7月,实验结果表明比特率相较于H.264/AVC High Profile平均降低20%左右,这些结果促使MPEG与VCEG合作发起的新的标准化工作。

2010年1月,VCEG和MPEG开始发起视频压缩技术正式提案。相关技术由视频编码联合组审议和评估,其合作小组第一次会议于2010年4月召开大会,一共有27个完整的提案。评价结果表明,一些提案在许多测试用例可以达到只用一半的比特率并维持H.264相同的视觉质量。在这次会议上,联合项目名改称为高效率的视频编码(HEVC),并且JCT-VC小组把相关技术集成到一个软件代码库(HM)和标准文本草案规范,并进行进一步实验,以评估各项功能。

2012年2月10日,在美国圣何塞召开了第99届MPEG会议。MPEG组织和ITU-T组织对JCT-VC的工作表示满意,准备于2013年1月,同时在ISO/IEC和ITU-T发布HEVC标准的最终版本。

2013年1月26日,HEVC正式成为国际标准。总结HEVC发展的标准时间点:

  • 2010年1月,ITU-T VCEG和ISO/IEC MPEG联合成立JCT-VC联合组织,统一制定下一代编码标准:HEVC;

  • 2012年2月,委员会草案通过了HEVC;

  • 2012年7月,HEVC国际标准草案获得通过;

  • 2013年1月,国际标准最终获得通过。

1.3 应用领域

伴随着每次视频压缩技术的进化,会产生多种影像服务和产品的诞生,如图1.1所示。1995年实现标准化的MPEG-2得到了DVD和数字电视等领域采用,大幅扩大了视频压缩技术的应用范围。MPEG-4在1998年实现标准化后,立即应用到了移动和互联网视频服务领域。伴随视频压缩技术的升级,各种影像服务和产品随之登场。2013年以后,随着HEVC的出现,4K及8K电视及网络全高清影像服务也纷纷出现。箭头指示的是各服务和产品主要采用的压缩技术。

图****1.1 视频压缩技术及对应的影像服务和产品的历史

HEVC的应用示意图如图1.2所示。在广播电视、网络视频服务、电影院及公共大屏幕等众多领域,4K×2K和8K×4K视频发送将变得更容易实现。个人电脑及智能手机等信息终端自不用说,平板电视、摄像机及数码相机等AV产品也会支持HEVC。不仅是这些既有市场,HEVC还有可能在今后有望增长的新市场上大显身手。例如,影像监控系统就是其中之一。影像监控系统最近几年在快速从原来的模拟摄像头组合VTR的方式,向经由IP网络发送、存储和浏览数码摄像头拍摄的视频的方法过渡。为提高安全性,需要增加摄像头数量、提高影像的精细度,而与此同时,确保网络频带和存储容量增加。估计HEVC将作为解决这些课题的措施而被得到应用。

1.2 HEVC的应用示例

1.4 优缺点

优点:

  • 高压缩率:在视频质量相同的条件下,较264平均减少50%的码流,可以节省大量的网络带宽及存储空间;可以在相同码流的条件下提供更加高质量的视频
  • 支持8192×4320分辨率

缺点:

  • HEVC使用到的技术和算法较前两代标准264和MPEG-2更为复杂,视频流在压缩过程中需要更多的选择和运算。
  • HEVC不支持大多数硬件,通常需要效率更高,更多的处理器来辅助,这意味着,如果有一个固件需要更新,而编解码器却跟不上升级速度的话,那么我们的电视机顶盒和蓝光播放机是无法播放HEVC编码内容的,需要等待解决方案出现后才能继续使用。

2 基础概念及算法

2.1 码率

视频码率是视频数据(视频色彩量、亮度量、像素量)每秒输出的位数。一般用的单位是kbps。

在视频会议应用中,视频质量和网络带宽占用是矛盾的,通常情况下视频流占用的带宽越高则视频质量也越高;如要求高质量的视频效果,那么需要的网络带宽也越大;解决这一矛盾的钥匙当然是视频编解码技术。评判一种视频编解码技术的优劣,是比较在相同的带宽条件下,哪个视频质量更好;在相同的视频质量条件下,哪个占用的网络带宽更少。

码率越高理论上意味着质量越好,但是在肉眼分辨的范围内,码率高到一定程度就没什么差别了。所以码率设置有最优值,以H.264文档中视频建议的码率:

视频大小分辨率建议码率
480P720X4801800Kbps
720P1280X7203500Kbps
1080P1920X10808500Kbps

2.2 帧率

是用于测量显示帧数的量度。所谓的测量单位为每秒显示帧数或“赫兹”。区别与码率,帧率是记录显示帧数的量度,而码率是数据传输时单位时间传送的数据位数。

由于人类眼睛的特殊生理结构,如果所看画面之帧率高于16的时候,就会认为是连贯的,此现象称之为视觉停留。这也就是为什么电影胶片是一格一格拍摄出来,然后快速播放的。

而对游戏,一般来说,第一人称射击游戏比较注重FPS的高低,如果FPS<30的话,游戏会显得不连贯。所以有一句有趣的话:“FPS(指FPS游戏)重在FPS(指帧率)。

每秒的帧数(fps)或者说帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说30fps就是可以接受的,但是将性能提升至60fps则可以明显提升交互感和逼真感,但是一般来说超过75fps一般就不容易察觉到有明显的流畅度提升了。

如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过刷新率的帧率就浪费掉了。

2.3 量化参数QP

QP是量化步长(Qstep)的序号,对于亮度编码而言,量化步长共有52个值,QP取值051,对于色度编码QP的取值039。

量化参数,反映了空间细节压缩情况。值越小,量化越精细,图像质量越高,产生的码流也越长。QP 小,大部分的细节都会被保留,码率增大。QP 增大,一些细节丢失,码率降低,但图像失真加强和质量下降。也就是说,QP 和比特率成反比的关系,而且随着视频源复杂度的提高,这种反比关系会更明显。

2.4 GOP

GOP(Group Of Pictures, 图像组)是一组连续的图像,由一个I帧和多个B/P帧组成,是编解码器存取的基本单位。GOP结构常用的两个参数M和N,M指定GOP中首个P帧和I帧之间的距离,N指定一个GOP的大小。例如M=1,N=15,GOP结构为:IPBBPBBPBBPBBPB,GOP有两种:闭合式GOP和开放式GOP:

闭合式GOP:闭合式GOP只需要参考本GOP内的图像即可,不需参考前后GOP的数据。这种模式决定了,闭合式GOP的显示顺序总是以I帧开始以P帧结束

开放式GOP :开放式GOP中的B帧解码时可能要用到其前一个GOP或后一个GOP的某些帧。码流里面包含B帧的时候才会出现开放式GOP。

开放式GOP和闭合式GOP中I帧、P帧、B帧的依赖关系如下图所示:

2.5 失真

衡量失真的三种准则:

  • 均方差MSE
  • 信噪比SNR
  • 峰值信噪比PSNR

2.6 量化

1、大概的公式:l = floor(c/Qstep + f),c表示系数、Qstep表示量化步长、l表示量化后的值,floor是向下取整函数,f控制舍入关系

2、HEVC有52个量化步长,对应了52个量化参数QP,可以通过查表查询

3、对于色度分量,量化参数限制为045。具体的说,当亮度分量的QP小于30时,色度分量的QP和亮度的相同,当亮度信号QP在3051时,两者的关系可以通过查表得出

4、量化过程同时要完成整数DCT中的比例缩放运算,为了避免浮点计算,HEVC把分子分母进行放大处理,然后取整,以此保证运算精度,QP的运算方式也要进行调整:QP = floor(QP/6) + QP % 6

5、总的量化公式:

2.7 率失真优化

1、分层递归遍历所有的CU划分模式,选取最优的CU划分模式,这个过程称为CU划分模式判别

2、对于其中的每一个CU,遍历所有的PU模式,选取最优的PU模式,并在此基础上选择最优的TU模式

3、对于其中的每一个PU,遍历所有的预测模式,选取最优的预测模式,这个过程称为预测模式判别

(1)帧内预测模式。

​ ①遍历所有的预测模式,得到每种模式下的残差信号,再对残差信号进行Hadamard变换计算SATD值

​ ②利用SATD值计算每种预测模式的率失真代价,选取率失真代价最小的几种模式(与PU大小相关)为预测模式集

​ ③将已编码相邻块的预测模式补充到预测模式集中

​ ④遍历模式集合中的所有模式,并对残差信号进行正常编码(熵编码),计算率失真代价

​ ⑤选取最优的预测模式作为该PU的最优模式

​ ⑥当亮度块的模式确定之后,把该模式以及DC、planar、水平方向模式、垂直方向模式作为色度块的候选模式,选取最优的模式即可

(2)帧间预测模式。

​ HEVC采用了Merge和AMVP技术,更加高效地表示帧间预测参数,但是这两种方式有比较大的区别,因此帧间预测可以分成Merge帧间预测模式和非Merge帧间预测模式(采用AMVP技术),分别选取最优的Merge帧间预测模式和非Merge帧间预测模式,然后从中选取最优的模式。

​ 对于Merge帧间预测模式,遍历所有的候选模式,计算率失真代价,选择率失真代价最小的模式为最优模式。当采用merge帧间预测模式I按摩,且预测残差信号的编码比特数为零时,只需要编码skip标识和merge索引两个语法元素,此时该模式称为MODE_SKIP模式

​ 对于非Merge帧间预测模式(采用AMVP技术)

​ ①根据AMVP技术确定MVP列表,计算每个MVP的率失真代价,得到最优的MVP

​ ②以最优MVP为起始点,进行整像素运动预测,得到最优的整像素运动矢量

​ ③以整像素运动矢量为中心,进行半像素搜索,从周围的8个点确定最优的半像素精度运动矢量

​ ④以半像素精度矢量为中心,进行1/4像素精度的运动搜索,确定最优的1/4像素精度运动矢量

(3)PU模式判别

​ ①计算2Nx2N模式的率失真代价,将其作为最优代价,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将2Nx2N作为最优的PU模式

​ ②如果CU的深度已经取得最大值,且inter_4x4_enabled_flag是1,那么执行③,否则执行④

​ ③计算NxN模式的率失真,更新最优代价和模式,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将NxN作为最优的PU模式

​ ④计算Nx2N模式的率失真代价,更新最优代价和模式,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将Nx2N作为最优的PU模式

​ ⑤计算2NxN模式的率失真代价,更新最优代价和模式,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将2NxN作为最优的PU模式

​ ⑥如果TestAMP_Hor为1,当前CU执行水平方向上的AMP模式,否则执行本步骤。计算2NxnU模式的率失真代价,更新最优代价和模式,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将2NxnU作为最优的PU模式;否则计算2NxnD模式的率失真代价,更新最优的代价和模式,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将2NnD作为最优的PU模式

​ ⑦如果TestAMP_Ver为1,当前CU执行垂直方向上的AMP模式,否则执行本步骤。计算nLx2N模式的率失真代价,更新最优模式和代价,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将nLx2N作为最优的PU模式;否则计算nRx2N模式的率失真代价,更新最优模式和代价,如果预测残差的编码比特数为零,那么满足CBF_Fast,直接跳至⑪,并将nRx2N作为最优的PU模式

​ ⑧注意上面的都是帧间的模式,现在需要计算帧内2Nx2N模式的率失真代价,更新最优模式和代价

​ ⑨如果当前CU深度为最大值,计算帧内NxN模式的率失真代价,更新最优模式和代价

​ ⑩如果当前CU大于或等于PCM模式所允许的最小单元,并且代价大于PCM模式,那么更新最优模式和代价

​ ⑪结束搜索,得到最优的PU模式。注意对于每一种PU模式,都要夯实不同的TU划分,选取最优的TU模式,因此最优的PU模式包含了最优的TU模式

(4)CU划分模式判决

​ ①把CTU作为CU,计算率失真代价(最优PU模式时),此时应包含CU分割标志(split_flag)的编码比特数

​ ②对CU进行四叉树划分,计算每个子CU的最小率失真代价,并对所有子CU的率失真代价求和,得到该CU的率失真代价

​ ③为每个子CU作为CU重复②

​ ④重复③直到编码的最大深度

​ ⑤从最大的编码深度,比较4个子CU与CU的率失真代价,一次选出最优的划分方式。

​ ⑥对于每个CU,如果其帧间2Nx2N模式为MODE_SKIP,则满足Early_SKIP条件,直接结束该CU的进一步划分

3 编解码技术

3.1 H.265编码框架及编码单元结构

与H.263以来的编码标准一样,HEVC的设计沿用了经典的基于块的混合视频编码框架。框架主要包括,帧内预测、帧间预测、转换、量化、去区块滤波器、熵编码等模块,但在HEVC编码架构中,整体被分为了三个基本单位,分别是:编码单位(coding unit,CU)、预测单位(predict unit,PU)和转换单位(transform unit,TU)。

1.3 HEVC编码框架

视频编码的基本流程为:将视频序列的每一帧划为固定大小的宏块,通常为16×16像素的亮度分量及2个8×8像素的色度分量,之后以宏块为单位进行编码。对视频序列的第一帧及场景切换帧或者随机读取帧采用I帧编码方式,I帧编码只利用当前帧内的像素空间预测,类似于JPEG图像编码方式。其大致过程为,利用帧内先前已经编码中的像素对当前块内的像素值作出预测(对应图中的帧内预测模块),将预测值与原始视频信号作差运算得到预测残差,再对预测残差进行变换、量化及熵编码形成编码码流。对其余帧采用帧间编码方式,包括前向预测P帧和双向预测B帧,帧间编码是对当前帧内的块在先前已编码帧中寻找最相似块(运动估计)作为当前块的预测值(运动补偿),之后如I帧的编码过程对预测残差进行编码。编码器中还含有一个解码器,模拟解码过程以获得解码重构图像,作为编码下一帧或下一块的预测参考。解码步骤包括对变换量化后的系数激进型反量化、反变换,得到预测残差,之后预测残差与预测值相加,经滤波去除块效应后得到解码重构图像。

图****1.4 帧内预测编码图

图****1.5 帧间预测编码图

HEVC以LCU块为单位对输入视频进行处理,首先是预测,有两种模式:帧内预测与帧间预测。

帧内预测,即利用当前图像内已编码像素生成预测值;

帧间预测,即利用当前图像之前已编码图像的重建像素生成预测值。

3.2 编码单元结构

H.264通常会以16×16像素为单位,将图片划分为多个大小相同的宏块,并以这些宏块作为编码时的最小元素。H.265则是将切割画面的工作从使用者手工设定,转交给编码器来决定,让编码器视情况以16×16、32×32、64×64等尺寸,将画面切割为数个编码树单元,一般来说区块尺寸越大,压缩效率就越好。

图****2.1 左图是H.264标准,每个宏块大小都是固定的;

右图是H.265标准,编码单元大小是根据区域信息量来决定的

H.265没有沿用之前H.264之前宏块的概念,而是使用编码单元(CU)作为及基本的编码结构。一个CU可以包含一个或多个不同尺寸的预测单元PU,一个PU包含若干个变换单元(TU)。CU、PU、TU三种在编码中起的作用不一样,不过这种编码方式还是基于混合编码,只是采用这种划分方式能够更好地分割一幅图像,用于后续的预测和处理。采用这种结构设计的目的是在增加灵活性的同时,使压缩预测更符合图像特性。

  1. 编码基本单元(CU)

CU的特点是方块,在LCU基础上划分的,通常LCU的大小64×64,可以使用递归分割四叉树的方法来得到,大的CU适用于图像中比较平滑的部分,而小的CU则适用于边缘和纹理较丰富的区域。采用大尺寸CU主要是为了高清压缩编码的应用,毕竟如1080p甚至更大分辨率的视频图像,其空间会有更大面积的一致性,因此采用更大的编码单元能更有效地减少空间冗余。

编码单元是否被划分取决于分割标志位split flag。0表示不再进行四叉树划分,1表示继续划分为4个独立的编码单元。

图****2.2 图像划分结构示意图

如果仍采用光栅扫描方式对CU寻址会很不方便,因此,H.265定义了Z扫描顺序,如图2.3所示。这种顺序保证了对于不同分割都能按照相同的遍历顺序进行寻址有利于程序中递归实现。

2.3 Z扫描方式

2.预测单元(PU)

PU是基本的预测单元,是在编码单元CU的基础上进行划分的,有skip、intra、inter三种模式可以分割,每个CU中可以包含一个或者多个PU。PU可以是正方形的,也可以是长方形的,这是为了能够更好地区分背景与物体,如图2.4所示。

2.4 PU划分图

如图2.5所示,PU的划分方式可分为对称及不对称两种,要注意的是PU的尺寸不能超过其所属的CU。不对称的划分方式主要适用于CU中纹理偏差比较大的情况,增加预测的精准度,不对称的PU仅适用于帧间预测。

2.5 PU划分方式

3.变换单元(TU)

TU是变换及量化的基本单元,它可以大于PU但是不能大于CU的大小。TU同样采用二叉树的分割结构,所支持的尺寸从4×4至32×32的大小。TU的形状取决于PU的划分模式,当PU为正方形时,TU也是正方形的,当PU是长方形的,TU也是长方形的,一个CU可以包含一个或多个TU。

根据预测残差的局部变化特性,TU可以自适应地选择最优的模式。大块的TU模式能够将能量更好地集中,小块的TU模式能够保存更多的图像细节。这种灵活的分割结构,可以使变换后的残差能量得到充分压缩,以进一步提高编码增益。

2.6 TU划分图

总结:如图2.7,可以形象地展示CU、PU及TU之间的关系。

图****2.7 划分关系

3.3 帧内预测

利用图像的空间相关性,用周围重建像素值对当前编码块进行预测 。

H.265更多的帧内预测方向,在H.264采用9个帧内预测方向的场合,,H.265预测方向拓展到33个,另外加上一个DC和一个planar,一共35中预测模式,使得预测更加精细,增加更多提升更高效帧内压缩的可能的参考像素块。明显的代价是在增加的方向中搜索需要更多编码时间。

图****2.8 帧内预测模式

Planar模式

平面预测是一种新提出的预测方法,常用于内容平滑或纹理不清晰的单元。它为预测单元中的每一个像素点也都要进行插值预测,如图所示。首先根据左侧相邻单元的右下角像素和上方相邻单元的下边界像素插值出当前预测单元下边界的每个像素点,再根据上方相邻单元的右下角像素和左侧相邻单元的右边界像素插值出当前预测单元右边界的每个像素点,然后利用上方相邻单元的下边界、左侧相邻单元的右边界以及插值出的当前单元的下边界和右边界插值出其余的像素点。

2.9 Planar插值预测

3.4 帧间预测

帧间预测利用连续图像之间的相关性,通过运动估计和运动补偿的编码方法去消除视频信息的时间冗余。利用先前已编码重建帧作为参考帧进行预测。

  • 帧间预测采用融合模式时,当前PU块的运动信息(包括运动矢量、参考索引、预测模式)都可以通过相邻PU的运动信息推导得到。编码时,当前PU块只需要传送融合标记(Merge Flag)以及融合索引(Merge Index),无需传送其运动信息。

  • 帧间预测还可以通过空域相邻PU以及时域相邻PU的运动矢量信息构造出一个预测运动矢量候选表,PU遍历运动矢量候选列表,在其中选择最佳的预测运动矢量。

3.4.1 广义B帧预测技术

在高效预测模式下,H.265仍然采用H.264中的等级B帧预测方式,同时还增加了广义B帧(GPB)预测方式取代低时延应用场景中的P帧预测方式。GPB预测结构是指传统P帧采取类似与B帧的双向预测方式进行预测。在这种预测方式下,前向和后向参考列表中的参考图像必须为当前图像之前的图像,且两者为同一图像。对P帧采取B帧的运动预测方式增加了运动估计的准确度,提高了编码效率,同时也有利于编码流程的统一。

3.4.2 去块滤波(Debtock)

去块滤波位于反变换之后,主要是去除诗篇压缩过程中产生的方块效应。首先对垂直边界进行水平滤波,先亮度块后色度块;再对水平边界进行垂直滤波,先亮度块后色度块。H.265对8x8块的边界进行滤波,与H.264中对4x4边的边界进行滤波相比,H.265中去块滤波算法的时间复杂度有所降低。

3.4.3 采样点自适应偏移(SAO)

把一帧划分为若干个LCU,然后对每个LCU中每个像素进行SAO操作,将根据其LCU像素特征选择一种像素补偿方式,以减少源图像与重构图像之间的失真。自适应样点补偿方式分为带状补偿(BO)和边缘补偿(EO)两类。

  1. 带状补偿

带状补偿将像素值强度等级划分为若干个条带,每个条带内的像素拥有相同的补偿值。进行补偿时根据重构像素点处的条带,选择相应的带状补偿值进行补偿。

现有的HM模型将像素值强度从0到最大值划分为32个等级。同时这32个等级条带还分为两类,第一类是位于中间的16个条带,剩余的16个条带是第二类。编码时只将其中一类具有较大补偿值的条带偏移信息写入片头;另一类条带信息则不传送,这样的方式编码将具有较小偏移值的一类条带忽略不计,从而节省了编码比特数。

\\2. 边缘补偿

边缘补偿主要用于对图像的轮廓进行补偿。它将当前像素值与相邻的2个像素值进行对比,用于比较的2个相邻像素可以在下图中所示的4中模板中选择,从而得到该像素点的类型。解码端根据码流中标识的像素点的类型信息进行相应的补偿校正。

3.4.4 自适应环路滤波(ALF)

ALF在编解码环路内,位于Debtock和SAO之后,用于恢复重建图像以达到重建图像与原始图像之间的均方差(MSE)最小。ALF的系数是在帧级计算和传输的,可以整帧应用ALF,也可以对于基于块或基于量化树的部分区域进行ALF,如果是基于部分区域的ALF,还必须传递指示区域信息的附加信息。

对于亮度分量,采用CU为单位的四叉树ALF结构。滤波使用5x5,7x7和9x9三种大小的二维钻石型模板。滤波器计算每个4x4块的Laplacian系数值,并根据该值将所有4x4块分成16类,分别对应16种滤波器。

2.10 ALF滤波模板

对于色度分量,滤波时色度分量统一使用5x5矩形滤波模板,不需要通过Laplacian系数来选择滤波器类型。

3.5 并行设计

当前芯片架构已经从单核性能逐渐往多核并行方向发展,因此为了适应并行化程度非常高的芯片实现,H.265引入了很多并行运算的优化思路。

3.5.1 Tile

用垂直和水平的边界将图像划分为一些行和列,划分出的矩形区域为一个Tile,每个Tile包含整数个LCU,Tile之间可以互相独立,以此实现并行处理。

2.11 Tile划分示意图

3.6 熵编码

H.265的熵编码只采取基于上下文的二进制熵编码算法(CABAC),在本质上与H.264的CABAC是一致的,只是在实现细节上有些差别。H.265减少了上下文的数量,以改进熵编码的性能和编码速度。

3.7 变换量化

H.265的变换支持4x4到32x32,比H.264增加了16x16和32x32两种变换核。在H.264中,一个宏块只能采用一种变换核,而H.265提供了残差四叉树(RQT)的递归变换结构,对于一个CU或者PU,可以采用多种变换核。

另外,对于4x4的TU,H.265提供了跳过变换模式,在这种模式中,预测残差只进行移位。对于帧间预测的4x4变换块,H.265还提供了离散正弦变换(DST)。在量化方面,H.265采用了与H.264相同的量化方法,H.265还提供了率失真优化量化方法(RDOQ)方法。率失真优化量化就是在量化过程中引入率失真优化选择的思想,具体可以分为三个步骤:

  1. 对当前处理的TU,以4x4的块为单位进行扫描,对于4x4量化块的每个量化值,分别加一减一,这样就得到三个量化值,根据率失真代价最小准则对4x4量化块的每一个点选择最佳的量化值;

  2. 对于扫描到的每一个4x4的量化块,将其量化值设置为零,并与之前的率失真代价比较,选择一种最佳的量化方式;

  3. 对于当前TU来说,若最后一个非零系数的位置距离当前一个非零系数的位置较远,则将最后一个非零系数改为零,同时比较这种方式下的率失真代价并与之前的率失真代价进行比较,选择一种最佳的方式。

4 码流结构分析

4.1 重要参数

4.1.1 视频参数集VPS(Video Parameter Set)

VPS主要用于传输视频分级信息,有利于兼容标准在可分级视频编码或多视点视频的扩展。

  1. 用于解释编码过的视频序列的整体结构,包括时域子层依赖关系等。HEVC中加入该结构的主要目的是兼容标准在系统的多子层方面的扩展,处理比如未来的可分级或者多视点视频使用原先的解码器进行解码但是其所需的信息可能会被解码器忽略的问题。
  2. 对于给定视频序列的某一个子层,无论其SPS相不相同,都共享一个VPS。其主要包含的信息有:多个子层或操作点共享的语法元素;档次和级别等会话关键信息;其他不属于SPS的操作点特定信息。
  3. 编码生成的码流中,第一个NAL单元携带的就是VPS信息

4.1.2 序列参数集SPS(Sequence Parameter Set)

包含一个CVS中所有编码图像的共享编码参数。

  1. 一段HEVC码流可能包含一个或者多个编码视频序列,每个视频序列由一个随机接入点开始,即IDR/BLA/CRA。序列参数集SPS包含该视频序列中所有slice需要的信息;
  2. SPS的内容大致可以分为几个部分:1、自引ID;2、解码相关信息,如档次级别、分辨率、子层数等;3、某档次中的功能开关标识及该功能的参数;4、对结构和变换系数编码灵活性的限制信息;5、时域可分级信息;6、VUI。

4.1.3 图像参数集PPS(Picture Parameter Set)

包含一幅图像所用的公共参数,即一幅图像中所有片段SS(Slice Segment)引用同一个PPS。

  1. PPS包含每一帧可能不同的设置信息,其内容同H.264中的大致类似,主要包括:1、自引信息;2、初始图像控制信息,如初始QP等;3、分块信息。
  2. 在解码开始的时候,所有的PPS全部是非活动状态,而且在解码的任意时刻,最多只能有一个PPS处于激活状态。当某部分码流引用了某个PPS的时候,这个PPS便被激活,称为活动PPS,一直到另一个PPS被激活。

参数集包含了相应的编码图像的信息。SPS包含的是针对一连续编码视频序列的参数(标识符seq_parameter_set_id、帧数及POC的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等)。PPS对应的是一个序列中某一幅图像或者某几幅图像 ,其参数如标识符pic_parameter_set_id、可选的seq_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。

通常,SPS 和PPS 在片的头信息和数据解码前传送至解码器。每个片的头信息对应一个pic_parameter_set_id,PPS被其激活后一直有效到下一个PPS被激活;类似的,每个PPS对应一个seq_parameter_set_id,SPS被其激活以后将一直有效到下一个SPS被激活。参数集机制将一些重要的、改变少的序列参数和图像参数与编码片分离,并在编码片之前传送至解码端,或者通过其他机制传输。

4.2 NALU type

前缀码后面跟随的前两个字节为NALU的语法元素,主要有四个部分组成:

  • forbidden_zero_bit = 0:占1个bit,与H.264相同,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
  • nal_unit_type = 32:占6个bit,用来用以指定NALU类型
  • nuh_reserved_zero_6bits = 0:占6位,预留位,要求为0,用于未来扩展或3D视频编码
  • nuh_temporal_id_plus1 = 1:占3个bit,表示NAL所在的时间层ID

NALU的语法元素由H264的一个字节变为两个字节,而nal_unit_type则为NALU的类型,因此我们可以通过以下获取NALU的类型:

int type = (code & 0x7E)>>1;

nal_unit_typeNALU类型备注
0NAL_UNIT_CODE_SLICE_TRAIL_N非关键帧
1NAL_UNIT_CODED_SLICE_TRAIL_R
2NAL_UNIT_CODED_SLICE_TSA_N
3NAL_UINT_CODED_SLICE_TSA_R
4NAL_UINT_CODED_SLICE_STSA_N
5NAL_UINT_CODED_SLICE_STSA_R
6NAL_UNIT_CODED_SLICE_RADL_N
7NAL_UNIT_CODED_SLICE_RADL_R
8NAL_UNIT_CODED_SLICE_RASL_N
9NAL_UNIT_CODE_SLICE_RASL_R
10 ~ 15NAL_UNIT_RESERVED_X保留
16NAL_UNIT_CODED_SLICE_BLA_W_LP关键帧
17NAL_UNIT_CODE_SLICE_BLA_W_RADL
18NAL_UNIT_CODE_SLICE_BLA_N_LP
19NAL_UNIT_CODE_SLICE_IDR_W_RADL
20NAL_UNIT_CODE_SLICE_IDR_N_LP
21NAL_UNIT_CODE_SLICE_CRA
22 ~ 31NAL_UNIT_RESERVED_X保留
32NAL_UNIT_VPSVPS(Video Paramater Set)
33NAL_UNIT_SPSSPS
34NAL_UNIT_PPSPPS
35NAL_UNIT_ACCESS_UNIT_DELIMITER
36NAL_UNIT_EOS
37NAL_UNIT_EOB
38NAL_UNIT_FILLER_DATA
39NAL_UNIT_SEIPrefix SEI
40NAL_UNIT_SEI_SUFFIXSuffix SEI
41 ~ 47NAL_UNIT_RESERVED_X保留
48 ~ 63NAL_UNIT_UNSPECIFIED_X未规定
64NAL_UNIT_INVALID

type值所代表的类型:VPS=32 SPS=33 PPS=34 IDR=19 P=1 B=0

2.12 H.265输出码流

如上我们看到了四个NALU包,每个NALU的头部信息为:

① 00 00 00 01 40 01 —> (0x40 & 0x7E)>>1 = 32 —> VPS

② 00 00 00 01 42 01 —> (0x42 & 0x7E)>>1 = 33 —> SPS

③ 00 00 00 01 44 01 —> (0x44 & 0x7E)>>1 = 34 —> PPS

④ 00 00 00 01 26 01 —> (0x26 & 0x7E)>>1 = 19 —> IDR

Android音视频——H265编码核心技术解析

一、前言

音视频开发需要你懂得音视频中一些基本概念,针对编解码而言,我们必须提前懂得编解码器的一些特性,码流的结构,码流中一些重要信息如sps,pps,vps,start code以及基本的工作原理,而大多同学都只是一知半解,所以导致代码中的部分内容虽可以简单理解却不知其意,所以,在这里总结出了当前主流的H.265编码相关的原理 。

二、定义

H.265(HEVC High Efficiency Video Coding)是现行H.264标准于2003年实现标准化以来时隔10年推出的新标准,将成为支撑未来十年的影像服务和产品的视频压缩技术。其特点是,支持1080p以上的4K×2K和8K×4K分辨率,将视频压缩率提高至H.264的约2倍。也就是说,能以原来一半的编码速度发送相同画质的视频。例如,按照20Mbit/秒发送的H.264格式视频内容,在相同画质的条件下用HEVC格式只需10Mbit/秒的速度。

三、编码的分类

  • 软件编码(简称软编):使用CPU进行编码。
  • 硬件编码(简称硬编):不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。

优缺点

  • 软编:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
  • 硬编:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。

四、编码原理

视频编码的目的是为了压缩原始视频,压缩的主要思路是从空间、时间、编码、视觉等几个主要角度去除冗余信息。由于 H.264 出色的数据压缩比率和视频质量,成为当前市场上最为流行的编解码标准。而 H.265 是在 H.264 的基础上,保证相同视频质量的同时,视频流的码率还可以减少 50%。随着 H.265 编码格式越来越流行,本文将主要介绍 H.265 的编码原理,以下是 H.265 的编码框架流程图。

该框架中,帧间预测和帧内预测共同组成了编码器的预测模块,帧内预测根据单帧图像内部的像素特点,压缩图像内部的冗余,消除空间上的相关性。帧间预测通过根据特定参考帧,来计算当前帧的运动矢量,用来消除时间上的相关性。一般对于视频中的第一帧,由于编码器中还没有相应的参考帧可用,将仅使用帧内编码模式编码,之后才可以用帧间编码方式。

当预测模块产生预测信息后,编码器将预测信息与当前帧信息做差值得到残差数据,并对所得到的残差数据进行一系列的量化变换处理,进一步消除冗余信息,最后将其与其他信息合在一起就构成了最终的编码好的数据流。而对于框架中的反量化、反变以及滤波等过程其实与解码器中的对应功能相同,即在编码器中部分存在着解码器的部分功能,这一部分的主要功能是根据变换和量化处理后的残差信息重建原始残差信息,这些原始残差信息将与一些预测信息一起,作为帧内预测所需要的数据。或者经过一系列的滤波处理,加上运动估计,作为帧间预测中的参考帧。《音视频入门到精通学习》

五、H.265核心技术

1、四叉树编码结构

该结构使用编码单元(Coding Unit, CU) ,预测单(Prediction Unit, PU)和变换单元(Transform unit, TU) 3个概念描述整个编码过程。
●编码单元: HEVC定义了5种类型的编码元: 128x128(LCU) , 64x64 , 32x32 ,16x16 , 8x 8(Smallest Coding Unit, SCU)。
●对于每个CU , HEVC使用PU来实现该CU单元的预测过程,对于帧内预测,HEVC定义了34种帧内预测方向( H.264为9种) , 对于帧间预测, HEVC采取了运动矢量方案(MVR )、差值滤波(IF)、运动共享(MS)、运动向量竞争(MVC)和基于块的照明竞争(B- BIC)来提高编码性能。
●变换单元,则是针对正交变换和量化。对于正交变换,HEVC采用包含了16x16 , 32x 32和64x 64等尺寸块的变换矩阵、旋转变换和基于模式的方向性变换来提高编码性能。

HEVC的变换结构突破了原有的变换尺寸限制,可支持4 x4至32x32的编码变换,以TU为基本单元进行变换和量化。为提高大尺寸编码单元的编码效率, DCT变换同样采用四叉树型的变换结构。下图为编码单元、变换单元的四叉树结构关系图,其中虚线为变换单元四叉树分割,实线为编码单元四叉树分割,编号为各编码单元的编码顺序。

左图是传统的H.264标准,每个宏块大小都是固定的;右图是H.265标准,编码单元大小是根据区域信息量来决定的。

2、H.265/HEVC 的帧内预测

在帧内预测放面,H.265 标准中采用的帧内预测算法原理与 H.264/AVC 基本标准一致。不同的是在 H.265 中,支持的预测模式达到了 35 种,包括 33 种角度预测以及不带方向性的 Planar 和 DC 两种模式,如下图所示。由于使用了基于四叉树的编码结构,所以在帧内预测时的参考值并不局限于当前编码单元左上方到右上方之间的像素值,有了更大的参考元素空间。HEVC 在帧内预测模式的过程中会更具不同的 PU 大小选择不同的帧内预测模式,这种选择方式更加细致,并且能提高帧内预测的精准度,最终反映为冗余空间大小的降低。

在帧间预测中,H.265/HEVC 主要在如下四个方面做了改进:

(1)H.265/HEVC 仍然采用了 H.264 中的 B 预测方式,同时还增加了广义 B(GPB,Generalized P and B picture)预测方式。对于单个 P 帧,H.265/HEVC 使用一个向前参考列表和向后参考列表对其进行帧间预测,两个列表中的图像相同并且在序列上均位于该帧之前。

(2)H.265/HEVC 的编码器对像素的大小进行了拓展,具体分为 8 bit、10bit、12bit。通过这种方式,可以提升每个像素中包含的信息量,从而提升解码的效果。

(3)H.265/HEVC 提出了融合模式(Merge Mode),它融合了之前使用的它将以往预测过程中的跳过模式(Skip Mode)和直接模式(Direct Mode)的过程。当使用这种模式时,某个 PU 块的运动信息可以通过与之相邻的 PU 块的运动信息进行推导来得到,这些信息包括:运动适量、参考索引等等

(4)自适应运动矢量预测技术(AMVP,Adaptive Motion Vector Predition):这种预测方式的作用对象为普通的帧间预测 PU。使用 AMVP 时,PU 通过对运动矢量的候选列表来选择最合适的预测运动适量,而运动矢量候选表则是通过相邻的 PU 以及相邻空域和时域的 PU 的信息由 AMVP 进行构造。

3、H.265环路滤波

由于 H.265 采用分块编码,在图像反量化、反变换重建的时候,会存在一些失真效应,例如块效应、振铃效应。为了解决这些问题,H.265 采用了环路滤波技术,其中包括去方块滤波(DBF)和样点自适应补偿(SAO)

DBF 作用于边界像素,用于解决块效应。块效应是指一些相邻编码块边界处的灰度值存在明显的不连续性,产生块效应主要有两个原因:

  • 编码器对残差的 DCT 变换和量化是基于块的,忽略了块与块之间的相关性,导致块之间的处理不一致;

  • 帧间预测运动补偿块的不完全匹配,存在误差;而编码时的预测参考帧通常来自这些重建图像,导致待预测图像失真;

DBF 针对边界类型采用强滤波、弱滤波或者不处理,边界类型的判定是由边界像素梯度阈值和边界块的量化参数决定的。DBF 处理时,先对整个图像的垂直边缘进行水平滤波,然后对水平边缘进行垂直滤波。滤波过程实际上就是对像素值进行修正的过程,让方块看起来不那么明显。H.264 中也存在 DBF 技术,但是应用于 44 大小的处理块,而 H.265 中应用于 88 大小的处理块。

SAO 是 H.265 新引入的对重建图像的误差补偿机制,用于改善振铃效应。振铃效应是指图像的灰度值剧烈变化产生的震荡,产生振铃效应主要原因是 DCT 变换后高频信息丢失。SAO 的原理就是通过对重构曲线的波峰像素添加负值补偿,波谷添加正值补偿,从而减小高频信息的失真。和 DBF 只作用于边界像素不同,SAO 作用于块中所有的像素

4、H.265熵编码

HEVC的熵编码只采用了基于上下文的二进制熵编码算法(
CABAO,在本质上与.H.264/AVC的CABAC是一致的,只是在实现细节上有些差别。HEVC减少了上下文的数量,以改进熵编码的性能和编码速度。

5、H.265细粒度slice分块边界

H.265的熵编码slice边界划分不以LCU为单位,而是以更小的CU为单位,每个slice的大小都可以精确控制,同时解决了码率控制和负载均衡的问题。但是带来的代价是slice边界处理更为复杂。片的分割如图:

6、广义B帧预测技术

在高效预测模式下,H.265 仍然采用H.264 中的等级B预测方式,同时还增加了广义B(Generalized P and Bpicture, GPB) 预测方式取代低时延应用场景中的P预测方式。GPB预测结构”是指对传统P帧采取类似于B 帧的双向预测方式进行预测。在这种预测方式下,前向和后向参考列表中的参考图像都必须为当前图像之前的图像,且两者为同一图像。对P帧采取B帧的运动预测方式增加了运动估计的准确度,提高了编码效率,同时也有利于编码流程的统一。

7、去块滤波(Deblock filter)

去块滤波位于反变换之后,主要 是去除视频压缩过程中产生的方块效应。首先对垂直边界进行水平滤波,先亮度块后色度块;再对水平边界进行垂 直滤波,先亮度块后色度块。 HEVC对8x8块的边界进行滤波,与H.264/AVC 中对4x4 边的边界进行滤波相比,HEVO中 去块滤波算法的时间复杂度有所降低。

六、编码实战代码

1.实现流程

  • 初始化相机参数,设置相机代理,这里就固定只有竖屏模式。
  • 初始化编码器参数,并启动编码器
  • 在编码成功的回调中从开始录制200帧(文件大小可自行修改)的视频,存到沙盒中,能够经过链接数据线到电脑从itunes中将文件(test0.asf)提取出来

2.编码器实现流程

  • 建立编码器须要的session (h264, h265 或同时建立)

  • 设置session属性,如实时编码,码率,fps, 编码的分辨率的宽高,相邻I帧的最大间隔等等

    • 注意H265目前不支持码率的限制
  • 当相机回调AVCaptureVideoDataOutputSampleBufferDelegate采集到一帧数据的时候则使用H264/H265编码器对每一帧数据进行编码。

  • 若编码成功会触发回调,回调函数首先检测是否有I帧出现,若是有I帧出现则将sps,pps信息写入不然遍历NALU码流并将startCode替换成0x00, 0x00, 0x00, 0x01

3.主要方法解析

  • 初始化编码器 首先选择使用哪一种方式实现,在本例中能够设置[XDXHardwareEncoder getInstance].enableH264 = YES 或者 [XDXHardwareEncoder getInstance].enableH265 = YES,也能够同时设置,若是同时设置须要将其中一个回调函数中的writeFile的方法屏蔽掉,而且只有较新的iPhone(> iPhone8 稳定)才支持同时打开两个session。

判断当前设备是否支持H265编码,必须知足两个条件,一是iPhone 7 以上设备,二是版本大于iOS 11git

if (@available(iOS 11.0, *)) 
            BOOL hardwareDecodeSupported = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
            if (hardwareDecodeSupported) 
                _deviceSupportH265 = YES;
                NSLog(@"XDXHardwareEncoder : Support H265 Encode/Decode!");
            
        else 
            _deviceSupportH265 = NO;
            NSLog(@"XDXHardwareEncoder : Not support H265 Encode/Decode!");
        
复制代码

系统已经提供VTIsHardwareDecodeSupported判断当前设备是否支持H265编码github

初始化编码器操做算法

- (void)prepareForEncode 
    if(self.width == 0 || self.height == 0) 
        NSLog(@"XDXHardwareEncoder : VTSession need with and height for init,with = %d,height = %d",self.width, self.height);
        return;
    
    
    if(g_isSupportRealTimeEncoder)  NSLog(@"XDXHardwareEncoder : Device processor is 64 bit");
    else                            NSLog(@"XDXHardwareEncoder : Device processor is not 64 bit");
    
    NSLog(@"XDXHardwareEncoder : Current h264 open state : %d, h265 open state : %d",self.enableH264, self.enableH265);
    
    OSStatus h264Status,h265Status;
    BOOL isRestart = NO;
    if (self.enableH264) 
        if (h264CompressionSession != NULL) 
            NSLog(@"XDXHardwareEncoder : H264 session not NULL");
            return;
        
        [m_h264_lock lock];
        NSLog(@"XDXHardwareEncoder : Prepare H264 hardware encoder");
        
        //[self.delegate willEncoderStart];
        
        self.h264ErrCount = 0;
        
        h264Status = VTCompressionSessionCreate(NULL, self.width, self.height, kCMVideoCodecType_H264, NULL, NULL, NULL, vtCallBack,(__bridge void *)self, &h264CompressionSession);
        if (h264Status != noErr) 
            self.h265ErrCount++;
            NSLog(@"XDXHardwareEncoder : H264 VTCompressionSessionCreate Failed, status = %d",h264Status);
        
        [self getSupportedPropertyFlags];
        
        [self applyAllSessionProperty:h264CompressionSession propertyArr:self.h264propertyFlags];
        
        h264Status = VTCompressionSessionPrepareToEncodeFrames(h264CompressionSession);
        if(h264Status != noErr) 
            NSLog(@"XDXHardwareEncoder : H264 VTCompressionSessionPrepareToEncodeFrames Failed, status = %d",h264Status);
        else 
            initializedH264     = true;
            NSLog(@"XDXHardwareEncoder : H264 VTSession create success, with = %d, height = %d, framerate = %d",self.width,self.height,self.fps);
        
        if(h264Status != noErr && self.h264ErrCount != 0) isRestart = YES;
        [m_h264_lock unlock];
    
    
    if (self.enableH265) 
        if (h265CompressionSession != NULL) 
            NSLog(@"XDXHardwareEncoder : H265 session not NULL");
            return;
        
        [m_h265_lock lock];
        NSLog(@"XDXHardwareEncoder : Prepare h265 hardware encoder");
        // [self.delegate willEncoderStart];
        
        self.h265ErrCount = 0;
        
        h265Status = VTCompressionSessionCreate(NULL, self.width, self.height, kCMVideoCodecType_HEVC, NULL, NULL, NULL, vtH265CallBack,(__bridge void *)self, &h265CompressionSession);
        if (h265Status != noErr) 
            self.h265ErrCount++;
            NSLog(@"XDXHardwareEncoder : H265 VTCompressionSessionCreate Failed, status = %d",h265Status);
        
        
        [self getSupportedPropertyFlags];
        
        [self applyAllSessionProperty:h265CompressionSession propertyArr:self.h265PropertyFlags];
        
        h265Status = VTCompressionSessionPrepareToEncodeFrames(h265CompressionSession);
        if(h265Status != noErr) 
            NSLog(@"XDXHardwareEncoder : H265 VTCompressionSessionPrepareToEncodeFrames Failed, status = %d",h265Status);
        else 
            initializedH265     = true;
            NSLog(@"XDXHardwareEncoder : H265 VTSession create success, with = %d, height = %d, framerate = %d",self.width,self.height,self.fps);
        
        if(h265Status != noErr && self.h265ErrCount != 0) isRestart = YES;
        [m_h265_lock unlock];
    
    
    if (isRestart) 
        NSLog(@"XDXHardwareEncoder : VTSession create failured!");
            static int count = 0;
            count ++;
            if (count == 3) 
                NSLog(@"TVUEncoder : restart 5 times failured! exit!");
                return;
            
            sleep(1);
            NSLog(@"TVUEncoder : try to restart after 1 second!");
            NSLog(@"TVUEncoder : vtsession error occured!,resetart encoder width: %d, height %d, times %d",self.width,self.height,count);
            [self tearDownSession];
            [self prepareForEncode];
    

复制代码

1> g_isSupportRealTimeEncoder = (is64Bit == 8) ? true : false;用来判断当前设备是32位仍是64位bash

2> 建立H264/H265Session 区别仅仅为参数的不一样,h264为kCMVideoCodecType_H264。 h265为kCMVideoCodecType_HEVC,在建立Session指定了回调函数后,当编码成功一帧就会调用相应的回调函数。服务器

3> 经过[self getSupportedPropertyFlags];获取当前编码器支持设置的属性,通过测试,H265不支持码率的限制。目前暂时得不到解决。等待苹果后续处理。网络

4> 以后设置编码器相关属性,下面会具体介绍,设置完成后则调用VTCompressionSessionPrepareToEncodeFrames准备编码。session

  • 设置编码器相关属性
- (OSStatus)setSessionProperty:(VTCompressionSessionRef)session key:(CFStringRef)key value:(CFTypeRef)value 
    OSStatus status = VTSessionSetProperty(session, key, value);
    if (status != noErr)  
        NSString *sessionStr;
        if (session == h264CompressionSession) 
            sessionStr = @"h264 Session";
            self.h264ErrCount++;
        else if (session == h265CompressionSession) 
            sessionStr = @"h265 Session";
            self.h265ErrCount++;
        
        NSLog(@"XDXHardwareEncoder : Set %s of %s Failed, status = %d",CFStringGetCStringPtr(key, kCFStringEncodingUTF8),sessionStr.UTF8String,status);
    
    return status;


- (void)applyAllSessionProperty:(VTCompressionSessionRef)session propertyArr:(NSArray *)propertyArr 
    OSStatus status;
    if(!g_isSupportRealTimeEncoder) 
        /* increase max frame delay from 3 to 6 to reduce encoder pressure*/
        int         value = 3;
        CFNumberRef ref   = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
        [self setSessionProperty:session key:kVTCompressionPropertyKey_MaxFrameDelayCount value:ref];
        CFRelease(ref);
    
    
    if(self.fps) 
        if([self isSupportPropertyWithKey:Key_ExpectedFrameRate inArray:propertyArr]) 
            int         value = self.fps;
            CFNumberRef ref   = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
            [self setSessionProperty:session key:kVTCompressionPropertyKey_ExpectedFrameRate value:ref];
            CFRelease(ref);
        
    else 
        NSLog(@"XDXHardwareEncoder : Current fps is 0");
    
    
    if(self.bitrate) 
        if([self isSupportPropertyWithKey:Key_AverageBitRate inArray:propertyArr]) 
            int value = self.bitrate;
            if (session == h265CompressionSession) value = 2*1000;  // if current session is h265, Set birate 2M.
            CFNumberRef ref = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
            [self setSessionProperty:session key:kVTCompressionPropertyKey_AverageBitRate value:ref];
            CFRelease(ref);
        
    else 
        NSLog(@"XDXHardwareEncoder : Current bitrate is 0");
    
    
    /*2016-11-15,@gang, iphone7/7plus do not support realtime encoding, so disable it
     otherwize ,we can not control encoding bit rate
     */
    if (![[self deviceVersion] isEqualToString:@"iPhone9,1"] && ![[self deviceVersion] isEqualToString:@"iPhone9,2"]) 
        if(g_isSupportRealTimeEncoder) 
            if([self isSupportPropertyWithKey:Key_RealTime inArray:propertyArr]) 
                NSLog(@"use RealTimeEncoder");
                NSLog(@"XDXHardwareEncoder : use realTimeEncoder");
                [self setSessionProperty:session key:kVTCompressionPropertyKey_RealTime value:kCFBooleanTrue];
            
        
    
    
    if([self isSupportPropertyWithKey:Key_AllowFrameReordering inArray:propertyArr]) 
        [self setSessionProperty:session key:kVTCompressionPropertyKey_AllowFrameReordering value:kCFBooleanFalse];
    
    
    if(g_isSupportRealTimeEncoder) 
        if([self isSupportPropertyWithKey:Key_ProfileLevel inArray:propertyArr]) 
            [self setSessionProperty:session key:kVTCompressionPropertyKey_ProfileLevel value:self.enableH264 ? kVTProfileLevel_H264_Main_AutoLevel : kVTProfileLevel_HEVC_Main_AutoLevel];
        
    else 
        if([self isSupportPropertyWithKey:Key_ProfileLevel inArray:propertyArr]) 
            [self setSessionProperty:session key:kVTCompressionPropertyKey_ProfileLevel value:self.enableH264 ? kVTProfileLevel_H264_Baseline_AutoLevel : kVTProfileLevel_HEVC_Main_AutoLevel];
        
        
        if (self.enableH264) 
            if([self isSupportPropertyWithKey:Key_H264EntropyMode inArray:propertyArr]) 
                [self setSessionProperty:session key:kVTCompressionPropertyKey_H264EntropyMode value:kVTH264EntropyMode_CAVLC];
            
        
    
    
    if([self isSupportPropertyWithKey:Key_MaxKeyFrameIntervalDuration inArray:propertyArr]) 
        int         value   = 1;
        CFNumberRef ref     = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
        [self setSessionProperty:session key:kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration value:ref];
        CFRelease(ref);
    

复制代码

上述方法主要设置启动编码器所需的各个参数数据结构

1.kVTCompressionPropertyKey_MaxFrameDelayCount : 压缩器被容许保持的最大帧数在输出一个压缩帧以前。例如若是最大帧延迟数是M,那么在编码帧N返回的调用以前,帧N-M必须被排出。app

2.kVTCompressionPropertyKey_ExpectedFrameRate : 设置fps

3.kVTCompressionPropertyKey_AverageBitRate : 它不是强制的限制,bit rate可能会超出峰值

4.kVTCompressionPropertyKey_RealTime : 设置编码器是否实时编码,若是设置为False则不是实时编码,视频效果会更好一点。

5.kVTCompressionPropertyKey_AllowFrameReordering : 是否让帧进行从新排序。为了编码B帧,编码器必须对帧从新排序,这将意味着解码的顺序与显示的顺序不一样。将其设置为false以防止帧从新排序。

6.kVTCompressionPropertyKey_ProfileLevel : 指定编码比特流的配置文件和级别

7.kVTCompressionPropertyKey_H264EntropyMode :若是支持h264该属性设置编码器是否应该使用基于CAVLC 仍是 CABAC

8.kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration : 两个I帧之间最大持续时间,该属性特别有用当frame rate是可变

  • 相机回调中对每一帧数据进行编码
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 
    if( !CMSampleBufferDataIsReady(sampleBuffer)) 
        NSLog( @"sample buffer is not ready. Skipping sample" );
        return;
    
    
    if([XDXHardwareEncoder getInstance] != NULL) 
        [[XDXHardwareEncoder getInstance] encode:sampleBuffer];
    

复制代码

以上方法在每采集到一帧视频数据后会调用一次,咱们将拿到的每一帧数据进行编码。

  • 编码具体实现
-(void)encode:(CMSampleBufferRef)sampleBuffer 
    if (self.enableH264) 
        [m_h264_lock lock];
        if(h264CompressionSession == NULL) 
            [m_h264_lock unlock];
            return;
        
        
        if(initializedH264 == false) 
            NSLog(@"TVUEncoder : h264 encoder is not ready\\n");
            return;
        
    
    
    if (self.enableH265) 
        [m_h265_lock lock];
        if(h265CompressionSession == NULL) 
            [m_h265_lock unlock];
            return;
        
        
        if(initializedH265 == false) 
            NSLog(@"TVUEncoder : h265 encoder is not ready\\n");
            return;
        
    
    
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CMTime duration = CMSampleBufferGetOutputDuration(sampleBuffer);
    frameID++;
    CMTime presentationTimeStamp = CMTimeMake(frameID, 1000);
    

    
    [self doSetBitrate];
    
    OSStatus status;
    VTEncodeInfoFlags flags;
    if (self.enableH264) 
        status = VTCompressionSessionEncodeFrame(h264CompressionSession, imageBuffer, presentationTimeStamp, duration, NULL, imageBuffer, &flags);
        if(status != noErr) NSLog(@"TVUEncoder : H264 VTCompressionSessionEncodeFrame failed");
        [m_h264_lock unlock];
        
        if (status != noErr) 
            NSLog(@"TVUEncoder : VTCompressionSessionEncodeFrame failed");
            VTCompressionSessionCompleteFrames(h264CompressionSession, kCMTimeInvalid);
            VTCompressionSessionInvalidate(h264CompressionSession);
            CFRelease(h264CompressionSession);
            h264CompressionSession = NULL;
        else 
            // NSLog(@"TVUEncoder : Success VTCompressionSessionCompleteFrames");
        
    
    
    
    
    if (self.enableH265) 
        status = VTCompressionSessionEncodeFrame(h265CompressionSession, imageBuffer, presentationTimeStamp, duration, NULL, imageBuffer, &flags);
        if(status != noErr) NSLog(@"TVUEncoder : H265 VTCompressionSessionEncodeFrame failed");
        [m_h265_lock unlock];
        
        if (status != noErr) 
            NSLog(@"TVUEncoder : VTCompressionSessionEncodeFrame failed");
            VTCompressionSessionCompleteFrames(h265CompressionSession, kCMTimeInvalid);
            VTCompressionSessionInvalidate(h265CompressionSession);
            CFRelease(h265CompressionSession);
            h265CompressionSession = NULL;
        else 
            NSLog(@"TVUEncoder : Success VTCompressionSessionCompleteFrames");
        
    
    
    

复制代码

1.经过frameID的递增构造时间戳为了使编码后的每一帧数据连续

2.设置最大码率的限制,注意:H265目前不支持设置码率的限制,等待官方后续通知。能够对H264进行码率限制

3.kVTCompressionPropertyKey_DataRateLimits : 将数据的bytes和duration封装到CFMutableArrayRef传给API进行调用

4.VTCompressionSessionEncodeFrame : 调用此方法成功后触发回调函数完成编码。

  • 回调函数中处理头信息
#pragma mark H264 Callback
static void vtCallBack(void *outputCallbackRefCon,void *souceFrameRefCon,OSStatus status,VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) 
    XDXHardwareEncoder *encoder = (__bridge XDXHardwareEncoder*)outputCallbackRefCon;
    if(status != noErr) 
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"H264: vtCallBack failed with %@", error);
        NSLog(@"XDXHardwareEncoder : encode frame failured! %s" ,error.debugDescription.UTF8String);
        return;
    
    
    if (!CMSampleBufferDataIsReady(sampleBuffer)) 
        NSLog(@"didCompressH265 data is not ready ");
        return;
    
    if (infoFlags == kVTEncodeInfo_FrameDropped) 
        NSLog(@"%s with frame dropped.", __FUNCTION__);
        return;
    
    
    CMBlockBufferRef block = CMSampleBufferGetDataBuffer(sampleBuffer);
    BOOL isKeyframe = false;

    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);

    if(attachments != NULL) 
        CFDictionaryRef attachment =(CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
        CFBooleanRef dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
        isKeyframe = (dependsOnOthers == kCFBooleanFalse);
    

    if(isKeyframe) 
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        static uint8_t *spsppsNALBuff = NULL;
        static size_t  spsSize, ppsSize;

            size_t parmCount;
            const uint8_t*sps, *pps;
            int NALUnitHeaderLengthOut;
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, &NALUnitHeaderLengthOut );
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, &NALUnitHeaderLengthOut );

            spsppsNALBuff = (uint8_t*)malloc(spsSize+4+ppsSize+4);
            memcpy(spsppsNALBuff, "\\x00\\x00\\x00\\x01", 4);
            memcpy(&spsppsNALBuff[4], sps, spsSize);
            memcpy(&spsppsNALBuff[4+spsSize], "\\x00\\x00\\x00\\x01", 4);
            memcpy(&spsppsNALBuff[4+spsSize+4], pps, ppsSize);
            NSLog(@"XDXHardwareEncoder : H264 spsSize : %zu, ppsSize : %zu",spsSize, ppsSize);
         writeFile(spsppsNALBuff,spsSize+4+ppsSize+4,encoder->_videoFile, 200);
    

    size_t blockBufferLength;
    uint8_t *bufferDataPointer = NULL;
    CMBlockBufferGetDataPointer(block, 0, NULL, &blockBufferLength, (char **)&bufferDataPointer);

    size_t bufferOffset = 0;
    while (bufferOffset < blockBufferLength - startCodeLength) 
        uint32_t NALUnitLength = 0;
        memcpy(&NALUnitLength, bufferDataPointer+bufferOffset, startCodeLength);
        NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
        memcpy(bufferDataPointer+bufferOffset, startCode, startCodeLength);
        bufferOffset += startCodeLength + NALUnitLength;
    
    writeFile(bufferDataPointer, blockBufferLength,encoder->_videoFile, 200);


#pragma mark H265 Callback
static void vtH265CallBack(void *outputCallbackRefCon,void *souceFrameRefCon,OSStatus status,VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) 
    XDXHardwareEncoder *encoder = (__bridge XDXHardwareEncoder*)outputCallbackRefCon;
    if(status != noErr) 
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"H264: H265 vtH265CallBack failed with %@", error);
        NSLog(@"XDXHardwareEncoder : H265 encode frame failured! %s" ,error.debugDescription.UTF8String);
        return;
    
    
    if (!CMSampleBufferDataIsReady(sampleBuffer)) 
        NSLog(@"didCompressH265 data is not ready ");
        return;
    
    if (infoFlags == kVTEncodeInfo_FrameDropped) 
        NSLog(@"%s with frame dropped.", __FUNCTION__);
        return;
    

    CMBlockBufferRef block = CMSampleBufferGetDataBuffer(sampleBuffer);
    BOOL isKeyframe = false;

    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);

    if(attachments != NULL) 
        CFDictionaryRef attachment =(CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
        CFBooleanRef dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
        isKeyframe = (dependsOnOthers == kCFBooleanFalse);
    

    if(isKeyframe) 
        CMFormatDescriptionRef format     = CMSampleBufferGetFormatDescription(sampleBuffer);
        static uint8_t *vpsspsppsNALBuff  = NULL;
        static size_t  vpsSize, spsSize, ppsSize;
            size_t parmCount;
            const uint8_t *vps, *sps, *pps;

            if (encoder.deviceSupportH265)        // >= iPhone 7 && support ios11
                CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vps, &vpsSize, &parmCount, 0);
                CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sps, &spsSize, &parmCount, 0);
                CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pps, &ppsSize, &parmCount, 0);

                vpsspsppsNALBuff = (uint8_t*)malloc(vpsSize+4+spsSize+4+ppsSize+4);
                memcpy(vpsspsppsNALBuff, "\\x00\\x00\\x00\\x01", 4);
                memcpy(&vpsspsppsNALBuff[4], vps, vpsSize);
                memcpy(&vpsspsppsNALBuff[4+vpsSize], "\\x00\\x00\\x00\\x01", 4);
                memcpy(&vpsspsppsNALBuff[4+vpsSize+4], sps, spsSize);
                memcpy(&vpsspsppsNALBuff[4+vpsSize+4+spsSize], "\\x00\\x00\\x00\\x01", 4);
                memcpy(&vpsspsppsNALBuff[4+vpsSize+4+spsSize+4], pps, ppsSize);
                NSLog(@"XDXHardwareEncoder : H265 vpsSize : %zu, spsSize : %zu, ppsSize : %zu",vpsSize,spsSize, ppsSize);
            
             writeFile(vpsspsppsNALBuff, vpsSize+4+spsSize+4+ppsSize+4,encoder->_videoFile, 200);
    

    size_t   blockBufferLength;
    uint8_t  *bufferDataPointer = NULL;
    CMBlockBufferGetDataPointer(block, 0, NULL, &blockBufferLength, (char **)&bufferDataPointer);

    size_t bufferOffset = 0;
    while (bufferOffset < blockBufferLength - startCodeLength) 
        uint32_t NALUnitLength = 0;
        memcpy(&NALUnitLength, bufferDataPointer+bufferOffset, startCodeLength);
        NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
        memcpy(bufferDataPointer+bufferOffset, startCode, startCodeLength);
        bufferOffset += startCodeLength + NALUnitLength;
    

     writeFile(bufferDataPointer, blockBufferLength,encoder->_videoFile, 200);

复制代码

1.首先在回调函数中截取到I帧,从I帧中提取到(h265中新增vps),sps,pps信息并写入文件

2.遍历其余帧将头信息0000,0001写入每一个头信息中,再将该数据写入文件便可

七、文末

以上是关于H.265的编码的一些核心技术点介绍,和编码的代码示例;或许还有许多关于编码的技术问题没有讲解到,有关更多Android音视频的技术入门及精通学习,可私信我学习哦!

以上是关于H.265视频编码原理总结的主要内容,如果未能解决你的问题,请参考以下文章

Codecs系列转载:手淘H265编解码算法与工程优化

Codecs系列转载:手淘H265编解码算法与工程优化

JavaCV升级1.5.6之后遇到h265/hevc编码的视频无法打开编解码器avcodec_open2() error -1:Could not open video codec异常解决办法

聊聊视频中的编解码器,你所不知道的h264h265vp8vp9和av1编解码库

JavaCV升级1.5.6之后遇到h265/hevc编码的视频无法打开编解码器avcodec_open2() error -1:Could not open video codec异常解决办法

H.265视频编码原理总结