Apple Developer VideoToolBox 官方文档
在iOS4.0苹果开始支持硬编解码,不过硬编解码在当时还属于私有API,不提供给开发者使用。
在2014年的WWDC大会上,也就是iOS8.0之后,苹果才放开了硬编解码的API。VideoToolbox.framework
是一套纯C语言的API,其中包含了很多C语言函数,同时VideoToolbox.framework
是基于Core Foundation
库函数,基于C语言VideoToolbox
实际上属于低级框架,它是可以直接访问硬件编码器与解码器,它存在与视频压缩与解压以及存储在像素缓存区中的数据转换提供服务。
硬编码的优点
- 提高编码性能(使用CPU的使用率大大降低,倾向使用CPU)
- 增加编码效率(将编码一帧的时间缩短)
- 延长电量使用(耗电量大大降低)
这个框架在音视频项目开发中,会频繁使用到。
VideoToolbox框架的流程
- 创建session
- 设置编码相关参数
- 循环获取采集数据
- 获取编码后数据
- 将数据写入H264文件
1、编码的输入与输出
在我们开始进行编码的工作之前,需了解VideoToolbox
进行编码的输入输出分别是什么?只有了解了这个,我们才能清楚知道如何去向VideoToolbox
添加数据,并且如何获取数据。
截屏2020-12-08 下午3.22.08.png
如图所示,左边的三帧视频帧是发送给编码器之前的数据,开发者必须将原始图像数据封装为CVPixelBuffer
的数据结构,该数据结构是使用VideoToolbox
的核心。关于CVPixelBuffer
的介绍可以去官方文档的了解。
Apple Developer CVPixelBuffer 官方文档
2、CVPixelBuffer 解析
在这个官方文档的介绍中,CVPixelBuffer的官方解释:是其主内存存储所有像素点数据的一个对象,那么什么是主内存? 其实它并不是我们平常所要操作的内存,它指的是存储区域存在于缓存之中,我们在访问这个块内存区域,需要先锁定这块的内存区域。
代码语言:javascript复制// 1.锁定内存区域:
CVPixelBufferLockBaseAddress(pixel_buffer, 0);
// 2.读取该内存区域数据到NSData对象中
Void *data = CVPixelBufferGetBaseAddress(pixel_buffer);
// 3.数据读取完毕后需要释放锁定区域
CVPixelBufferRelease(pixel_buffer);
单纯从它的使用方式,我们就可以知道这一块内存区域不是普通内存区域,它需要加锁、解锁等一系列操作。作为视频开发,尽量减少进行显存和内存的交换,所以在iOS开发过程中也要尽量减少对它的内存区域访问。建议使用iOS平台提供的对应的API来完成相应的一系列操作。在AVFoundation
回调方法中,它有提供我们的数据其实就是CVPixelBuffer
,只不过当时使用的是引用类型CVImageBufferRef
,其实就是CVPixelBuffer
的另外一个定义。Camera
返回的CVImageBuffer
中存储的数据是一个CVPixelBuffer
,而经过VideoToolbox
编码输出的CMSampleBuffer
中存储的数据是一个CMBlockBuffer
的引用。
截屏2020-12-08 下午4.07.30.png
在iOS中经常会使用到session
的方式,比如我们使用任何硬件设备都要使用对应的session
,麦克风就要使用到AudioSession
,使用Camera
就要使用AVCaptureSession
,使用编码则需要使用VTCompressionSession
。解码时,需要使用VTDecompressionSessionRef
。
3、视频编码步骤分解
第一步:使用VTCompressionSession方法,创建编码会话:
代码语言:javascript复制/*
参数1:NULL 分配器,设置NULL为默认分配
参数2:width
参数3:height
参数4:编码类型,如kCMVideoCodecType_H264
参数5:NULL encoderSpecification: 编码规范,设置NULL由VideoToolbox自己选择
参数6:NULL sourceImageBufferAttributes: 源像素缓冲区属性,设置NULL不让VideoToolbox创建,而是自己创建
参数7:NULL compressedDataAllocator: 压缩数据分配器,设置NULL为默认分配
参数8:回调 当VTCompressionSessionEncoderFrame被调用压缩一次后会被异步调用。注:当你设置NULL的时候,你需要调用VTCompressionSessionEncodeFrameWithOutputHandler方法进行压缩帧处理,支持iOS9.0以上
参数9:outputCallbackRefCon: 回调客户定义的参考值
参数10:compressionSessionOut: 编码会话变量
*/
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType
_H264,NULL, NULL, NULL, didCompressH264,
(__bridge void *) (self), &cEncodeingSession);
第二步:设置相关参数
代码语言:javascript复制/*
session:会话
propertykey::属性名称
propertyValue:属性值
*/
VT_EXPORT OSStatus
VTSessionSetProperty(
CM_NONNULL VTSessionRef session,
CM_NONNULL CFStringRef propertyKey,
CM_NONNULL CFTypeRef propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2) );
kVTCompressionPropertyKey_RealTime:
设置是否实时编码kVTProfileLevel_H264_Baseline_AutoLevel:
表示使用H264
的Profile
规格,可以设置Hight
的AutoLevel
规格kVTCompressionPropertyKey_AllowFrameReordering:
表示是否使用产生B帧数据。因为B帧在解码是非必要数据,所以开发过程中也可以抛弃B帧数据。kVTCompressionPropertyKey_MaxKeyFrameInterval:
表示关键帧的间隔,也就是我们常说的gop sizekVTCompressionPropertyKey_ExpectedFrameRate:
表示设置帧率kVTCompressionPropertyKey_AverageBitRate
/kVTCompressionPropertyKey_DataRateLimits
设置编码输出的码率
第三步:准备编码
代码语言:javascript复制// 开始编码
VTCompressionSessionPrepareToEncoderFrames(cEncodeingSession);
第四步:捕获编码数据
- 通过AVFoundation 捕获的视频,这个时候我们会走到AVFoundation捕获结果代理方法
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
// 获取视频流
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 开始视频录制,获取到摄像头的视频帧,传入encode方法中
dispatch_sync(cEncodeQueue, ^{
[self encode:sampleBuffer];
});
}
第五步:数据编码
- 将获取的视频数据编码
// 编码
- (void) encode:(CMSampleBufferRef )sampleBuffer
{
// 拿到每一帧为编码数据
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 设置帧时间,如果不设置会导致时间轴过长,时间戳以ms为单位
CMTime presentationTimeStamp = CMTimeMake(frameID , 1000);
VTEncodeInfoFlags flags;
/*
参数1:编码会话
参数2:未编码数据
参数3:获取到的这个sample buffer 数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳
参数4:对于获取到sample buffer数据,这个帧的展示时间,如果没有时间信息,可设置kCMTimeInvalid
参数5:frameProperties: 包含这个帧的属性,帧的改变会影响后边的编码帧
参数6:ourceFrameRefCon: 回调函数会有引用你设置的这个帧的参考值
参数7:infoFlagsOut: 指向一个VTEncodeInfoFlags来接受一个编码操作。如果使用异步运行,kVTEncodeInfo_Asynchronous被设置;同步运行,kVTEncdeInfo_FrameDropped被设置;设置NULL为不想接受这个信息
*/
OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
if (statusCode != noErr) {
NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d", (int)statusCode);
VTCompressionSessionInvalidate(cEncodeingSession);
CFRelease(cEncodeingSession);
cEncodeingSession = NULL;
return;
}
NSLog(@"H.264:VTCompressionSessionEncodeFrame Success");
}
第六步:编码数据处理-获取SPS/PPS
当编码成功后, 就会回调到最开始初始化编码器会话时传入的回调函数,回调函数的原型如下:
代码语言:javascript复制void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
- 判断status,如果成功则返回0(noErr);成功则继续处理,不成功则不处理。
- 判断是否关键帧
/*
为什么要判断关键帧?
因为VideoToolbox编码器在每一个关键帧前面都会输出SPS/PPS信息,所以如果本帧未关键帧,则可以取出对应的SPS/PPS信息。
// 判断当前是否为关键帧
CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
bool isKeyFrame = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
bool keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
*/
- 那么如何获取SPS/PPS信息?
// 判断当前帧是否为关键帧
// 获取SPS&PPS数据,只获取1次,保存在H264文件开头的第一帧中
// SPS(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位
// PPS
if (keyFrame) {
// 图像存储方式,编码器等格式描述
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
// SPS
size_t sparameterSetSize, sparameterSetCount;
const uint8_t * sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
if (statusCode == noErr) {
// 获取PPS
size_t pparameterSetSize, pparameterSetCount;
const uint8_t * pparameterSet;
// 从第一个关键帧获取SPS & PPS
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
// 获取H264参数集合中的SPS 和 PPS
if (statusCode == noErr) {
// found sps & pps
NSData * sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData * pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if (encoder) {
[encoder gotSpsPps:sps pps:pps];
}
}
}
第七步:编码压缩数据并写入H264文件
当我们获取了SPS/PPS信息之后,我们就获取实际内容来进行处理了
代码语言:javascript复制 CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length, totalLength;
char * dataPointer;
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
if (statusCodeRet == noErr) {
size_t bufferOffset = 0;
// 返回的nalu数据前4个字节不是001的statusCode,而是大端模式的帧长度length
static const int AVCCHeaderLength = 4;
// 循环获取nalu数据
while (bufferOffset < totalLength - AVCCHeaderLength) {
uint32_t NALUnitLength = 0;
// 读取 一单元长度的 nalu
memcpy(&NALUnitLength, dataPointer bufferOffset, AVCCHeaderLength);
// 从大端模式转换为系统端模式
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
// 获取nalu数据
NSData * data = [[NSData alloc] initWithBytes:(dataPointer bufferOffset AVCCHeaderLength) length:NALUnitLength];
// 将nalu数据写入到文件
[encoder gotEncoderData:data isKeyFrame:keyFrame];
// 读取下一个nalu 一次回调可能包含多个nalu数控
bufferOffset = AVCCHeaderLength NALUnitLength;
}
}
第一帧写入sps & pps
代码语言:javascript复制- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps {
NSLog(@"gotSpsPps %d %d", (int)[sps length], (int)[pps length]);
const char bytes[] = "x00x00x00x01";
size_t length = (sizeof bytes) -1;
NSData * ByteHeader = [NSData dataWithBytes:bytes length:length];
[fileHandele writeData:ByteHeader];
[fileHandele writeData:sps];
[fileHandele writeData:ByteHeader];
[fileHandele writeData:pps];
}
添加4个字节的H264协议 start code分割符 一般来说编码器编出的首帧数据为SPS & PPS
代码语言:javascript复制- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame {
NSLog(@"gotEncodedData %d", (int)[data length]);
if (fileHandele != NULL) {
// H264编码时,在每个NAL前添加起始码 0x000001,解码器在码流中检测起始码,当前NAL结束
/*
为防止NAL内部出现0x000001的数据,H264又提出“防止竞争 emulation prevention”机制,在编码完NAL时,如果检测出有连续两个0x00字节,就在后面插入一个0x03。当解码器在NAL内部检测到0x000003的数据,就把0x03抛弃。恢复原始数据。
总的来说H264的码流的打包方式有两种,一种为annex-b byte stream format 的格式,这个是绝大部分编码器富润默认输出格式,就是每个帧开头的3~4个字节是H264的start_code,0x00000001或者0x000001.
另一种是原始的NAL打包格式,就是开始的若干字节(1,2,4字节)是NAL的长度,而不是start_code,此时必须借助某个全局的数据来获得编码器的profile,level, PPS, SPS等信息才可以解码。
*/
const char bytes[] = "x00x00x00x01";
// 长度
size_t length = (sizeof bytes) -1;
// 头字节
NSData * ByteHeader = [NSData dataWithBytes:bytes length:length];
// 写入头字节
[fileHandele writeData:ByteHeader];
// 写入H264数据
[fileHandele writeData:data];
}
}
第八步:结束VideoToolBox
代码语言:javascript复制-(void)endVideoToolBox {
VTCompressionSessionCompleteFrames(cEncodeingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(cEncodeingSession);
CFRelease(cEncodeingSession);
cEncodeingSession = NULL;
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/179669.html原文链接:https://javaforall.cn