VideoToolBox 编码H265
在之前的文章里,我们写了使用VideoToolBox
编码H264
,本篇文章介绍还是通过VideoToolBox
编码H265
,在之前的Demo
上做一些稍微的调整即可达到编码H265裸流
的效果;
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头部格式如下:
与h264的nal层相比,
h265
的NAL 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.pngH265
帧类型与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编码器参数设置
源码介绍
- 创建
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;
}
- 提取关键帧中的
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");
}
}
- 后续将编码后的
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