Android 音视频01 --- H264的基本原理01

Posted

tags:

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

参考技术A

H264压缩技术主要采用了以下几种方法对视频数据进行压缩。包括:

解决的是空域数据冗余问题。

解决的是时域数据冗徐问题

将空间上的相关性变为频域上无关的数据然后进行量化。

经过压缩后的帧分为:I帧,P帧和B帧:

关键帧,采用帧内压缩技术。

向前参考帧,在压缩时,只参考前面已经处理的帧。采用帧音压缩技术。

双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。
除了I/P/B帧外,还有图像序列GOP。

H264的基本原理其实非常简单,下我们就简单的描述一下H264压缩数据的过程。通过摄像头采集到的视频帧(按每秒 30 帧算),被送到 H264 编码器的缓冲区中。编码器先要为每一幅图片划分宏块。

划分好宏块后,计算宏块的象素值。以此类推,计算一幅图像中每个宏块的像素值。

对于视频数据主要有两类数据冗余,一类是时间上的数据冗余,另一类是空间上的数据冗余。其中时间上的数据冗余是最大的。为什么说时间上的冗余是最大的呢?假设摄像头每秒抓取30帧,这30帧的数据大部分情况下都是相关联的。也有可能不止30帧的的数据,可能几十帧,上百帧的数据都是关联特别密切的。
H264编码器会按顺序,每次取出两幅相邻的帧进行宏块比较,计算两帧的相似度。如下图:

在H264编码器中将帧分组后,就要计算帧组内物体的运动矢量了。
H264编码器首先按顺序从缓冲区头部取出两帧视频数据,然后进行宏块扫描。当发现其中一幅图片中有物体时,就在另一幅图的邻近位置(搜索窗口中)进行搜索。如果此时在另一幅图中找到该物体,那么就可以计算出物体的运动矢量了。
运动矢量计算出来后,将相同部分(也就是绿色部分)减去,就得到了补偿数据。我们最终只需要将补偿数据进行压缩保存,以后在解码时就可以恢复原图了。压缩补偿后的数据只需要记录很少的一点数据。
我们把运动矢量与补偿称为 帧间压缩技术 ,它解决的是视频帧在时间上的数据冗余。除了帧间压缩,帧内也要进行数据压缩,帧内数据压缩解决的是空间上的数据冗余。

人眼对图象都有一个识别度,对低频的亮度很敏感,对高频的亮度不太敏感。所以基于一些研究,可以将一幅图像中人眼不敏感的数据去除掉。这样就提出了帧内预测技术。
一幅图像被划分好宏块后,对每个宏块可以进行 9 种模式的预测。找出与原图最接近的一种预测模式。然后,将原始图像与帧内预测后的图像相减得残差值。再将我们之前得到的预测模式信息一起保存起来,这样我们就可以在解码时恢复原图了,经过帧内与帧间的压缩后,虽然数据有大幅减少,但还有优化的空间。

可以将残差数据做整数离散余弦变换,去掉数据的相关性,进一步压缩数据。

上面的帧内压缩是属于有损压缩技术。也就是说图像被压缩后,无法完全复原。而CABAC属于无损压缩技术。
无损压缩技术大家最熟悉的可能就是哈夫曼编码了,给高频的词一个短码,给低频词一个长码从而达到数据压缩的目的。MPEG-2中使用的VLC就是这种算法,我们以 A-Z 作为例子,A属于高频数据,Z属于低频数据。看看它是如何做的。
CABAC也是给高频数据短码,给低频数据长码。同时还会根据上下文相关性进行压缩,这种方式又比VLC高效很多。

制定了相互传输的格式,将宏快 有组织,有结构,有顺序的形成一系列的码流。这种码流既可 通过 InputStream 网络流的数据进行传输,也可以封装成一个文件进行保存,主要作用是为了传输。

组成H264码流的结构中 包含以下几部分 ,从大到小排序依次是:
H264视频序列,图像,片组,片,NALU,宏块 ,像素。

NAL层:(Network Abstraction Layer,视频数据网络抽象层) : 它的作用是H264只要在网络上传输,在传输的过程每个包以太网是1500字节,而H264的帧往往会大于1500字节,所以要进行拆包,将一个帧拆成多个包进行传输,所有的拆包或者组包都是通过NAL层去处理的。
VCL层:(Video Coding Layer,视频数据编码层) : 对视频原始数据进行压缩

起始码0x 00 00 00 01 或者 0x 00 00 01 作为 分隔符
两个 0x 00 00 00 01之间的字节数据 是表示一个NAL Unit。

I 帧的特点:

1.分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多。
2.定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;
3.预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;
4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输。

1.更高的编码效率:同H.263等标准的特率效率相比,能够平均节省大于50%的码率。
2.高质量的视频画面:H.264能够在低码率情况下提供高质量的视频图像,在较低带宽上提供高质量的图像传输是H.264的应用亮点。
3.提高网络适应能力:H.264可以工作在实时通信应用(如视频会议)低延时模式下,也可以工作在没有延时的视频存储或视频流服务器中。
4.采用混合编码结构:同H.263相同,H.264也使用采用DCT变换编码加DPCM的差分编码的混合编码结构,还增加了如多模式运动估计、帧内预测、多帧预测、基于内容的变长编码、4x4二维整数变换等新的编码方式,提高了编码效率。
5.H.264的编码选项较少:在H.263中编码时往往需要设置相当多选项,增加了编码的难度,而H.264做到了力求简洁的“回归基本”,降低了编码时复杂度。
6.H.264可以应用在不同场合:H.264可以根据不同的环境使用不同的传输和播放速率,并且提供了丰富的错误处理工具,可以很好的控制或消除丢包和误码。
7.错误恢复功能:H.264提供了解决网络传输包丢失的问题的工具,适用于在高误码率传输的无线网络中传输视频数据。
8.较高的复杂度:264性能的改进是以增加复杂性为代价而获得的。据估计,H.264编码的计算复杂度大约相当于H.263的3倍,解码复杂度大约相当于H.263的2倍。
H.264的目标应用涵盖了目前大部分的视频服务,如有线电视远程监控、交互媒体、数字电视、视频会议、视频点播、流媒体服务等。H.264为解决不同应用中的网络传输的差异。定义了两层:视频编码层(VCL:Video Coding Layer)负责高效的视频内容表示,网络提取层(NAL:Network Abstraction Layer)负责以网络所要求的恰当的方式对数据进行打包和传送。

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音视频的技术入门及精通学习,可私信我学习哦!

以上是关于Android 音视频01 --- H264的基本原理01的主要内容,如果未能解决你的问题,请参考以下文章

Android 音视频编解码 -- 视频编码和H264格式原理讲解

Android 音视频编解码 -- 视频编码和H264格式原理讲解

Android自带的浏览器是不是支持h264编码的html5视频?

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android音视频H264编码基础