先来简单看下 音视频的采集 。
一、音视频的采集
音视频采集的核心流程
音/视频采集
- 用到的视频输出的类是AVCaptureVideoDataOutput音频输出的类是AVCaptureAudioDataOutput。
- 采集成功后的代理方法输出的音视频对象为CMSampleBufferRef类型的sampleBuffer。这里我们可以使用AVCaptureConnection来判断是音频还是视频。
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {if (connection self.audioConnection) { //音频}else if (connection self.videoConnection) { //视频}}
采集的核心流程跟 AVFoundation 拍照/录制视频 和 AVFoundation 人脸识别 的采集流程基本一致大家可以了解下。
二、视频的编解码
2.1 视频的编码
1.首先需要初始化编码器看代码
- (instancetype)initWithConfigure:(CQVideoCoderConfigure *)configure {self [super init];if (self) {self.configure configure;self.encodeQueue dispatch_queue_create("h264 hard encode queue", DISPATCH_QUEUE_SERIAL);self.callbackQueue dispatch_queue_create("h264 hard encode callback queue", DISPATCH_QUEUE_SERIAL);//1.创建编码session OSStatus status VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)self.configure.width, (int32_t)self.configure.height, kCMVideoCodecType_H264, NULL, NULL, NULL, compressionOutputCallback, (__bridge void * _Nullable)(self), if (status ! noErr) {NSLog("VTCompressionSessionCreate error status: %d", (int)status);return self;}//2、设置编码器参数//是否实时执行status VTSessionSetProperty(self.encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);NSLog("VTSessionSetProperty RealTime status: %d", (int)status);//指定编码比特流的配置文件和级别。直播一般使用baseline可减少由b帧减少带来的延迟。status VTSessionSetProperty(self.encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);NSLog("VTSessionSetProperty ProfileLevel status: %d", (int)status);//设置比特率均值比特率可以高于此。默认比特率为零表示视频编码器。应该确定压缩数据的大小。//注意比特率设置只在定时的时候有效status VTSessionSetProperty(self.encodeSesion, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFNumberRef)(self.configure.bitrate));NSLog("VTSessionSetProperty AverageBitRate status: %d", (int)status);//码率限制CFArrayRef limits (__bridge CFArrayRef)[(self.configure.bitrate / 4),(self.configure.bitrate * 4)];status VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits,limits);NSLog("VTSessionSetProperty DataRateLimits status: %d", (int)status);//设置关键帧间隔status VTSessionSetProperty(self.encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFNumberRef)(self.configure.fps * 2));NSLog("VTSessionSetProperty MaxKeyFrameInterval status: %d", (int)status);//设置预期的fpsCFNumberRef expectedFrameRate (__bridge CFNumberRef)(self.configure.fps);status VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate);NSLog("VTSessionSetProperty ExpectedFrameRate status: %d", (int)status);//3、准备编码status VTCompressionSessionPrepareToEncodeFrames(self.encodeSesion);NSLog("VTSessionSetProperty: set PrepareToEncodeFrames return: %d", (int)status);}return self;}
- 1、VTCompressionSessionCreate创建压缩会话并且添加了编码成功后的回调函数compressionOutputCallback。 参数1会话的分配器。传递NULL使用默认分配器。 参数2帧的宽度以像素为单位。如果视频编码器不支持所提供的宽度和高度系统可能会自动修改。 参数3帧的高度。 参数4编码类型。 参数5编码规范。NULL由videoToolbox自己选择。 参数6源像素缓冲区属性NULL不让videToolbox创建自己创建。 参数7 压缩数据分配器。NULL默认的分配。 参数8回调函数。异步调用。 参数9客户端为输出回调定义的引用值。这里传的事我们自定义的编码器也就是self。 参数10 要创建的编码会话对象。
- 2、VTSessionSetProperty属性配置。
- 3、VTCompressionSessionPrepareToEncodeFrames准备编码。
2、进行编码看代码
- (void)encoderSampleBuffers:(CMSampleBufferRef)sampleBuffer {CFRetain(sampleBuffer);dispatch_async(self.encodeQueue, ^{CVImageBufferRef imageBuffer (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);//帧数据self->frameID;CMTime timeStamp CMTimeMake(self->frameID, 1000);//该帧的时间戳CMTime duration kCMTimeInvalid;//持续时间VTEncodeInfoFlags flags;OSStatus status VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, timeStamp, duration, NULL, NULL, //编码if (status ! noErr) {NSLog("VTCompressionSessionEncodeFrame error status: %d",(int)status);}CFRelease(sampleBuffer);});}
- 1、CMSampleBufferGetImageBuffer从采集到的视频CMSampleBufferRef中获取CVImageBufferRef。
- 2、VTCompressionSessionEncodeFrame压缩编码 参数1:编码会话encodeSesion 参数2:CVImageBuffer对象包含视频帧数据 参数3:对应该帧的时间戳每个示例的时间戳必须大于前一个。 参数4:该演示帧的持续时间没有可填kCMTimeInvalid 参数5:编码该帧的键/值对属性信息。注意某些会话属性也可能在帧之间更改。这种变化对随后编码的帧有影响。 参数6:将要传递给回调函数的帧的引用值。 参数7:VTEncodeInfoFlags接收有关编码操作的信息.
3、编码成功后回调处理
// startCode 长度 4const Byte startCode[] "\x00\x00\x00\x01";void compressionOutputCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CM_NULLABLE CMSampleBufferRef sampleBuffer ) {if (status ! noErr) {NSLog("compressionOutputCallback error status: %d", (int)status);return;}if (!CMSampleBufferDataIsReady(sampleBuffer)) {NSLog("CMSampleBufferDataIsReady is not ready");return;}CQVideoEncoder *encoder (__bridge CQVideoEncoder *)outputCallbackRefCon;BOOL keyFrame NO;CFArrayRef attachmentsArray CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);keyFrame !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachmentsArray, 0), kCMSampleAttachmentKey_NotSync);//是否为关键帧并且有没有获取过sps 和 pps 数据。if (keyFrame const uint8_t *spsData, *ppsData;//获取图像原格式CMFormatDescriptionRef formatDes CMSampleBufferGetFormatDescription(sampleBuffer);OSStatus status1 CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDes, 0, OSStatus status2 CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDes, 1, if (status1 noErr noErr) {//sps/pps获取成功NSLog("Get sps and pps success");//sps 和 pps 数据只需保存在H264文件开头即可。encoder->hasSpsPps true;NSMutableData *spsDataM [NSMutableData dataWithCapacity:4 spsSize];[spsDataM appendBytes:startCode length:4];[spsDataM appendBytes:spsData length:spsSize];NSMutableData *ppsDataM [NSMutableData dataWithCapacity:4 ppsSize];[ppsDataM appendBytes:startCode length:4];[ppsDataM appendBytes:ppsData length:ppsSize];dispatch_async(encoder.encodeQueue, ^{if ([encoder.delegate respondsToSelector:selector(encodeCallbackWithSps:pps:)]) {[encoder.delegate encodeCallbackWithSps:spsDataM pps:ppsDataM];}});} else {NSLog("Get sps and pps failed, spsStatus:%d, ppsStatus:%d", (int)status1, (int)status2);}}//获取NAL Unit数据size_t lengthAtOffset, totalLength;char *dataPoint;//将数据复制到dataPointCMBlockBufferRef blockBuffer CMSampleBufferGetDataBuffer(sampleBuffer);OSStatus error CMBlockBufferGetDataPointer(blockBuffer, 0, if (error ! kCMBlockBufferNoErr) {NSLog("VideoEncodeCallback: get datapoint failed, status %d", (int)error);return;}//循环获取NAL Unit数据size_t offet 0;//返回的NAL Unit数据前四个字节不是系统端的startCode(0001)//而是大端模式的帧长度const int lengthStartCode 4;const int lengthBigFrame 4;while (offet 2.2视频的解码 解析H264格式数据 - (void)decodeH264Data:(NSData *)frame {dispatch_async(self.decodeQueue, ^{uint8_t *frameNALU (uint8_t *)frame.bytes;uint32_t lengthFrame (uint32_t)frame.length;int type (frameNALU[4] //0 01 00111 "type: %hhu, %d", frame[4], type);//将NAL Unit开始码转为4字节大端NAL Unit的长度信息。uint32_t naluSize lengthFrame - 4;uint8_t *pNaluSize (uint8_t *)(frameNALU[0] *(pNaluSize 3);frameNALU[1] *(pNaluSize 2);frameNALU[2] *(pNaluSize 1);frameNALU[3] *(pNaluSize);CVPixelBufferRef pixelBuffer NULL;switch (type) {case 0x05://I帧关键帧if ([self createDecoder]) {pixelBuffer [self decodeNALUFrame:frameNALU withFrameLength:lengthFrame];}break;case 0x06://增强信息break;case 0x07://spsself->_spsSize naluSize;self->_sps malloc(self->_spsSize);memcpy(self->_sps, break;case 0x08://ppsself->_ppsSize naluSize;self->_pps malloc(self->_ppsSize);memcpy(self->_pps, break;default://其他帧0x01到 0x05if ([self createDecoder]) {pixelBuffer [self decodeNALUFrame:frameNALU withFrameLength:lengthFrame];}break;}});} 创建解码器 - (BOOL)createDecoder {if (self.decodeSesion) return YES;const uint8_t * const parameterSetPointers[2] {_sps, _pps};const size_t parameterSetSize[2] {_spsSize, _ppsSize};int lengthStartCode 4;OSStatus status CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSize, lengthStartCode, if (status ! noErr) {NSLog("CMVideoFormatDescriptionCreateFromH264ParameterSets error status: %d", (int)status);return NO;}NSDictionary *decoderAttachments {(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], //摄像头的输出数据格式(id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:self.configure.width],(id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:self.configure.height],(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:YES]};//解码回调设置VTDecompressionOutputCallbackRecord decompressionCallback;decompressionCallback.decompressionOutputCallback decoderVideoOutputCallback;decompressionCallback.decompressionOutputRefCon (__bridge void * _Nullable)self;VTDecompressionSessionCreate(kCFAllocatorDefault, _decoderDesc, NULL, (__bridge CFDictionaryRef _Nullable)decoderAttachments, if (status ! noErr) {NSLog("VTDecompressionSessionCreate error status: %d", (int)status);return NO;}//实时编码status VTSessionSetProperty(_decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue); if (status ! noErr) {NSLog("VTSessionSetProperty RealTime error status:%d", (int)status);}return YES;} 解码 - (CVPixelBufferRef)decodeNALUFrame:(uint8_t *)frameNALU withFrameLength:(uint32_t)lengthFrame {CVPixelBufferRef outputPixelBuffer NULL;CMBlockBufferRef blockBufferOut NULL;CMBlockBufferFlags flag0 0;//1.OSStatus status CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frameNALU, lengthFrame, kCFAllocatorNull, NULL, 0, lengthFrame, flag0, if (status ! kCMBlockBufferNoErr) {NSLog("CMBlockBufferCreateWithMemoryBlock error status:%d", (int)status);return outputPixelBuffer;}CMSampleBufferRef sampleBuffer NULL;const size_t sampleSizeArray[] {lengthFrame};//2.创建sampleBufferstatus CMSampleBufferCreateReady(kCFAllocatorDefault, blockBufferOut, _decoderDesc, 1, 0, NULL, 1, sampleSizeArray, if (status ! noErr || !sampleBuffer) {NSLog("CMSampleBufferCreateReady error status:%d", (int)status);CFRelease(blockBufferOut);return outputPixelBuffer;}//解码VTDecodeFrameFlags decodeFrameFlags kVTDecodeFrame_1xRealTimePlayback;VTDecodeInfoFlags decodeInfoFlags kVTDecodeInfo_Asynchronous; //异步解码status VTDecompressionSessionDecodeFrame(_decodeSesion, sampleBuffer, decodeFrameFlags, if (status kVTInvalidSessionErr) {NSLog("VTDecompressionSessionDecodeFrame InvalidSessionErr status:%d", (int)status);} else if (status kVTVideoDecoderBadDataErr) {NSLog("VTDecompressionSessionDecodeFrame BadData status:%d", (int)status);} else if (status ! noErr) {NSLog("VTDecompressionSessionDecodeFrame status:%d", (int)status);}CFRelease(sampleBuffer);CFRelease(blockBuffer);return outputPixelBuffer;} 解码成功后回调函数 void decoderVideoOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,void * CM_NULLABLE sourceFrameRefCon,OSStatus status,VTDecodeInfoFlags infoFlags,CM_NULLABLE CVImageBufferRef imageBuffer,CMTime presentationTimeStamp,CMTime presentationDuration ) {if (status ! noErr) {NSLog("decoderVideoOutputCallback error status%d", (int)status);return;}CVPixelBufferRef *outputPixelBuffer (CVPixelBufferRef *)sourceFrameRefCon;*outputPixelBuffer CVPixelBufferRetain(imageBuffer);CQVideoDecoder *decoder (__bridge CQVideoDecoder *)decompressionOutputRefCon;dispatch_async(decoder.callbackQueue, ^{if ([decoder.delegate respondsToSelector:selector(videoDecodeCallback:)]) {[decoder.delegate videoDecodeCallback:imageBuffer];}//释放数据CVPixelBufferRelease(imageBuffer);});} 1、回调函数的参数 参数1:回调的引用值。 参数2:帧的引用值。 参数3:压缩失败/成功的状态码。 参数4:如果设置了kVTDecodeInfo_Asynchronous表示异步解码 如果设置了kVTDecodeInfo_FrameDropped可以丢帧 如果设置了kVTDecodeInfo_ImageBufferModifiable可以安全地修改imageBuffer实际图像的缓冲. 参数5:实际图像的缓冲。如果未设置kVTDecodeInfo_ImageBufferModifiable标志则视频解压缩器可能仍在引用此回调中返回的imageBuffer此时修改返回的imageBuffer是不安全的。 参数6:帧的时间戳。 参数7:帧的持续时间。 2、将指针*outputPixelBuffer指向实际图像缓冲区imageBuffer 3、将图像缓冲区imageBuffer回调出去用来展示。