美文网首页iOS Developer
VideoToolBox 编码H265

VideoToolBox 编码H265

作者: pengxiaochao | 来源:发表于2023-01-10 17:58 被阅读0次

    在之前的文章里,我们写了使用VideoToolBox编码H264,本篇文章介绍还是通过VideoToolBox编码H265,在之前的Demo上做一些稍微的调整即可达到编码H265裸流的效果;

    maxresdefault.jpeg

    H265 码流介绍

    H.265是新的编码协议,也即是H.264的升级版。H.265标准保留H.264原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置;

    具体介绍可以参考链接
    https://zhuanlan.zhihu.com/p/517888843

    H265 和H264 的区别

    已有基础的可以跳过本章节;
    需要更多细节介绍的可以参考链接

    1、版本
    H.265是新的编码协议,也即是H.264的升级版。H.265标准保留H.264原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置;

    2、降码率
    比起H.264/AVC,H.265/HEVC提供了更多不同的工具来降低码率,以编码单位来说,H.264中每个宏块(macroblock/MB)大小都是固定的16x16像素,而H.265的编码单位可以选择从最小的8x8到最大的64x64;

    3、新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置;

    4、采用了块的四叉树划分结构
    H.265相比H.264最主要的改变是采用了块的四叉树划分结构,采用了从64x64~8x8像素的自适应块划分,并基于这种块划分结构采用一系列自适应的预测和变换等编码技术;

    5、算法优化
    H264由于算法优化,可以低于1Mbps的速度实现标清数字图像传送;H265则可以实现利用1~2Mbps的传输速度传送720P(分辨率1280*720)普通高清音视频传送;

    6、同样的画质和同样的码率,H.265比H2.64 占用的存储空间要少理论50%;

    7、占用的存储空间缩小
    比起H.264/AVC,H.265/HEVC提供了更多不同的工具来降低码率,以编码单位来说,H.264中每个宏块(macroblock/MB)大小都是固定的16x16像素,而H.265的编码单位可以选择从最小的8x8到最大的64x64。那么,在相同的图象质量下,相比于H.264,通过H.265编码的视频大小将减少大约39-44%;

    H265 编码层结构

    1、H265 头部格式
    H265NALU头部格式如下:

    image.png
    与h264的nal层相比,h265NAL Unit Header有两个字节构成, 从图中可以看出HEVC的NAL包结构与h264有明显的不同,HEVC加入了nal所在的时间层的ID,去除了nal_ref_idc,字段解释如下:

    F:禁止位,1bit(最高位:15位),必须是0,为1标识无效帧

    Type: 帧类型,6bits(9~14位),0-31是vcl nal单元;32-63,是非vcl nal单元,VCL是指携带编码数据的数据流,而non-VCL则是控制数据流。

    image.png

    H265帧类型与H264不一样,其位置在第一个字节的1~6位(buf[0]&0x7E>>1),起始标识位00000001;常见的NALU类型:

    40 01,type=32,VPS(视频参数集)

    2 01,type=33,SPS(序列参数集)

    44 01,type=34,PPS(图像参数及)

    4E 01, type=39,SEI(补充增强信息)

    26 01,type=19,可能有RADL图像的IDR图像的SS编码数据 IDR

    02 01, type=01,被参考的后置图像,且非TSA、非STSA的SS编码数据

    VideoToolBox编码器参数设置

    源码介绍

    1. 创建VTCompressionSessionRef 的时候,需要判断系统是否支持H265,传入kCMVideoCodecType_HEVC 参数
    /// 判断 设备和参数是否需要支持H265
    - (BOOL)_deviceSupportH265Encode {
        if (@available(iOS 11, *)) {
            BOOL deviceSupportHEVCDecode = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
            if (deviceSupportHEVCDecode && self.enableH265) {
                return YES;
            }
            return NO;
        }
        return NO;
    }
    
     ///创建编码会话 
    /// kCMVideoCodecType_HEVC 
       OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_videoConfig.width, (int32_t)_videoConfig.height, kCMVideoCodecType_HEVC, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_vtSession);
       if (status != noErr) {
            NSLog(@"VTCompressionSession create failed. status=%d", (int)status);
            return self;
       }
    
    1. 提取关键帧中的vps/sps/pps等参数
    void VideoEncodeCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon,OSStatus status, VTEncodeInfoFlags infoFlags,  CMSampleBufferRef sampleBuffer ) {
        
        if (status != noErr) {
            NSLog(@"VideoEncodeCallback: encode error, status = %d", (int)status);
            return;
        }
        if (!CMSampleBufferDataIsReady(sampleBuffer)) {
            NSLog(@"VideoEncodeCallback: data is not ready");
            return;
        }
        VideoEncoder *encoder = (__bridge VideoEncoder *)(outputCallbackRefCon);
        
        //判断是否为关键帧
        BOOL keyFrame = NO;
        CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
        keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync);//(注意取反符号)
        
        //获取sps & pps 数据 ,只需获取一次,保存在h264文件开头即可
        if (keyFrame && !encoder.hasSpsPps) {
            
            size_t vpsSize, vpsCount;
            size_t spsSize, spsCount;
            size_t ppsSize, ppsCount;
            const uint8_t *vpsData, *spsData, *ppsData;
            //获取图像源格式
            CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
            OSStatus status0 = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(formatDesc, 0, &vpsData, &vpsSize, &vpsCount, 0);
            OSStatus status1 = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(formatDesc, 1, &spsData, &spsSize, &spsCount, 0);
            OSStatus status2 = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(formatDesc, 2, &ppsData, &ppsSize, &ppsCount, 0);
            
            //判断sps/pps获取成功
            if (status1 == noErr & status2 == noErr) {
                
                NSLog(@"VideoEncodeCallback: get sps, pps success");
                encoder.hasSpsPps = true;
                
                //vps data
                NSMutableData *vps = [NSMutableData dataWithCapacity:4 + spsSize];
                [vps appendBytes:startCode4 length:4];
                [vps appendBytes:vpsData length:vpsSize];
                
                //sps data
                NSMutableData *sps = [NSMutableData dataWithCapacity:4 + spsSize];
                [sps appendBytes:startCode4 length:4];
                [sps appendBytes:spsData length:spsSize];
                //pps data
                NSMutableData *pps = [NSMutableData dataWithCapacity:4 + ppsSize];
                [pps appendBytes:startCode4 length:4];
                [pps appendBytes:ppsData length:ppsSize];
                
                dispatch_async(encoder.callbackQueue, ^{
                    //回调方法传递sps/pps
                    [encoder.delegate videoEncodeCallbackVps:vps sps:sps pps:pps];
                });
                
            } else {
                NSLog(@"VideoEncodeCallback: get sps/pps failed spsStatus=%d, ppsStatus=%d", (int)status1, (int)status2);
            }
        }
        
        //获取NALU数据
        size_t lengthAtOffset, totalLength;
        char *dataPoint;
        
        //将数据复制到dataPoint
        CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        OSStatus error = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint);
        if (error != kCMBlockBufferNoErr) {
            NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)error);
            return;
        }
        
        //循环获取nalu数据
        size_t offet = 0;
        //返回的nalu数据前四个字节不是0001的startcode(不是系统端的0001),而是大端模式的帧长度length
        const int lengthInfoSize = 4;
        
        while (offet < totalLength - lengthInfoSize) {
            uint32_t naluLength = 0;
            //获取nalu 数据长度
            memcpy(&naluLength, dataPoint + offet, lengthInfoSize);
            //大端转系统端
            naluLength = CFSwapInt32BigToHost(naluLength);
            //获取到编码好的视频数据
            NSMutableData *data = [NSMutableData dataWithCapacity:4 + naluLength];
            [data appendBytes:startCode4 length:4];
            [data appendBytes:dataPoint + offet + lengthInfoSize length:naluLength];
            
            //将NALU数据回调到代理中
            dispatch_async(encoder.callbackQueue, ^{
                [encoder.delegate videoEncodeCallback:data];
            });
            
            //移动下标,继续读取下一个数据
            offet += lengthInfoSize + naluLength;
        }
      
    }
    

    3.将 VPS/SPS/PPS 写入文件头部

    /// vps/sps/pps 回调
    - (void)videoEncodeCallbackVps:(NSData *)vps sps:(NSData *)sps pps:(NSData *)pps {
        /// 这里的vps/sps/pps 都已经有了 起始码; 不用再加上,且文件必须先写vps/ sps pps ,再写NALU
        if (vps && sps && pps) {
            
            size_t vps_length = fwrite(vps.bytes, 1, vps.length, self.h265_file);
            if (vps_length != vps.length) {
                NSLog( @"write sps data error \n");
            }
            
            size_t sps_length = fwrite(sps.bytes, 1, sps.length, self.h265_file);
            if (sps_length != sps.length) {
                NSLog( @"write sps data error \n");
            }
            size_t pps_length = fwrite(pps.bytes, 1, pps.length, self.h265_file);
            if (sps_length != sps.length) {
                NSLog( @"write pps data error \n");
            }
            NSLog( @"write sps pps success \n");
        }
    }
    
    1. 后续将编码后的H265类型的NALU 单元写入文件尾部
    /// 编码器h265 类型的 NALU 回调
    -(void)videoEncodeCallback:(NSData *)h265Data {
       if (h265Data) {
           size_t nalu_length = fwrite(h265Data.bytes, 1, h265Data.length, self.h265_file);
           if (nalu_length != h265Data.length) {
               NSLog( @"write NALU data error");
           }
           NSLog( @"write NALU lenght:%lu \n",nalu_length);
       }
    }
    

    源码地址 源码地址: https://github.com/hunter858/OpenGL_Study/AVFoundation/VideoToolBox-encoderH265

    扩展

    相关文章

      网友评论

        本文标题:VideoToolBox 编码H265

        本文链接:https://www.haomeiwen.com/subject/hbqlcdtx.html