iOS Developer

VideoToolBox 编码H265

2023-01-10  本文已影响0人  pengxiaochao

在之前的文章里,我们写了使用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

扩展

上一篇下一篇

猜你喜欢

热点阅读