硬编码的优点
- 进步编码性能(运用CPU的运用率大大下降,倾向运用GPU)
- 增加编码功率(将编码一帧的时刻缩短)
- 延长电量运用(耗电量大大下降)
VideoToolBox结构的流程
- 创立session
- 设置编码相关参数
- 开端编码
- 循环获取收集数据
- 获取编码后数据
- 将数据写入H264文件
1.1 编码的输入和输出
如图所示,左边的三帧视频帧是发送給编码器之前的数据,开发者有必要将原始图画数据封装为CVPixelBuufer的数据结构.该数据结构是运用VideoToolBox的中心.
Apple Developer CVPixelBuffer 官方文档介绍
1.2 CVPixelBuffer 解析
在这个官方文档的介绍中,CVPixelBuffer 给的官方解释,是其主内存存储所有像素点数据的一个目标.那么什么是主内存了?
其实它并不是咱们平常所操作的内存,它指的是存储区域存在于缓存之中. 咱们在拜访这个块内存区域,需求先锁定这块内存区域.
//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
的引证.
在iOS中,会经常运用到session
的办法.比如咱们运用任何硬件设备都要运用对应的session
,麦克风就要运用AudioSession
,运用Camera
就要运用AVCaptureSession
,运用编码则需求运用VTCompressionSession
.解码时,要运用VTDecompressionSessionRef
.
1.3 视频编码过程分化
榜首步: 运用VTCompressionSessionCreate办法,创立编码会话;
//1.调用VTCompressionSessionCreate创立编码session
//参数1:NULL 分配器,设置NULL为默许分配
//参数2:width
//参数3:height
//参数4:编码类型,如kCMVideoCodecType_H264
//参数5:NULL encoderSpecification: 编码规范。设置NULL由videoToolbox自己挑选
//参数6:NULL sourceImageBufferAttributes: 源像素缓冲区特点.设置NULL不让videToolbox创立,而自己创立
//参数7:NULL compressedDataAllocator: 紧缩数据分配器.设置NULL,默许的分配
//参数8:回调 当VTCompressionSessionEncodeFrame被调用紧缩一次后会被异步调用.注:当你设置NULL的时分,你需求调用VTCompressionSessionEncodeFrameWithOutputHandler办法进行紧缩帧处理,支持iOS9.0以上
//参数9:outputCallbackRefCon: 回调客户界说的参考值
//参数10:compressionSessionOut: 编码会话变量
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession);
第二步:设置相关的参数
/*
session: 会话
propertyKey: 特点名称
propertyValue: 特点值
*/
VT_EXPORT OSStatus
VTSessionSetProperty(
CM_NONNULL VTSessionRef session,
CM_NONNULL CFStringRef propertyKey,
CM_NULLABLE 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 size. -
kVTCompressionPropertyKey_ExpectedFrameRate
: 表明设置帧率 -
kVTCompressionPropertyKey_AverageBitRate
/kVTCompressionPropertyKey_DataRateLimits
设置编码输出的码率.
第三步: 准备编码
//开端编码
VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
第四步: 捕获编码数据
- 通过AVFoundation 捕获的视频,这个时分咱们会走到AVFoundation捕获结果署理办法:
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//AV Foundation 获取到视频流
-(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被设置;同步运转,kVTEncodeInfo_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(@"H264:VTCompressionSessionEncodeFrame Success");
}
第六步: 编码数据处理-获取SPS/PPS
当编码成功后,就会回调到最开端初始化编码器会话时传入的回调函数,回调函数的原型如下:
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
-
判别status,假如成功则回来0(noErr) ;成功则持续处理,不成功则不处理.
-
判别是否关键帧
- 为什么要判别关键帧呢?
- 由于VideoToolBox编码器在每一个关键帧前面都会输出SPS/PPS信息.所以假如本帧未关键帧,则能够取出对应的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 pps & sps
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if(encoder)
{
[encoder gotSpsPps:sps pps:pps];
}
}
}
}
第七步 编码紧缩数据并写入H264文件
当咱们获取了SPS/PPS信息之后,咱们就获取实际的内容来进行处理了
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;
static const int AVCCHeaderLength = 4;//回来的nalu数据前4个字节不是001的startcode,而是大端形式的帧长度length
//循环获取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 gotEncodedData:data isKeyFrame:keyFrame];
//move to the next NAL unit in the block buffer
//读取下一个nalu 一次回调可能包括多个nalu数据
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
//榜首帧写入 sps & pps
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]);
const char bytes[] = "\x00\x00\x00\x01";
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];
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodeData %d",(int)[data length]);
if (fileHandele != NULL) {
//增加4个字节的H264 协议 start code 切割符
//一般来说编码器编出的首帧数据为PPS & SPS
//H264编码时,在每个NAL前增加开端码 0x000001,解码器在码流中检测开端码,当前NAL完毕。
/*
为了避免NAL内部出现0x000001的数据,h.264又提出'避免竞争 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[] ="\x00\x00\x00\x01";
//长度
size_t length = (sizeof bytes) - 1;
//头字节
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
//写入头字节
[fileHandele writeData:ByteHeader];
//写入H264数据
[fileHandele writeData:data];
}
}