赞
踩
先来简单看下 音视频的采集 。
音视频采集的核心流程:
音/视频采集
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), &_encodeSesion);
- 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);
- //设置预期的fps
- CFNumberRef 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;
- }
VTCompressionSessionCreate
:创建压缩会话,并且添加了编码成功后的回调函数compressionOutputCallback
。NULL
使用默认分配器。NULL
由videoToolbox
自己选择。NULL
不让videToolbox
创建,自己创建。NULL
默认的分配。self
。VTSessionSetProperty
属性配置。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, &flags);//编码
- if (status != noErr) {
- NSLog(@"VTCompressionSessionEncodeFrame error status: %d",(int)status);
- }
- CFRelease(sampleBuffer);
- });
- }
CMSampleBufferGetImageBuffer
从采集到的视频CMSampleBufferRef
中获取CVImageBufferRef
。VTCompressionSessionEncodeFrame
压缩编码:encodeSesion
CVImageBuffer
对象,包含视频帧数据kCMTimeInvalid
VTEncodeInfoFlags
接收有关编码操作的信息.3、编码成功后回调处理:
- // startCode 长度 4
- const 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 && !encoder->hasSpsPps) {
- size_t spsSize, spsCount, ppsSize, ppsCount;
- const uint8_t *spsData, *ppsData;
- //获取图像原格式
- CMFormatDescriptionRef formatDes = CMSampleBufferGetFormatDescription(sampleBuffer);
- OSStatus status1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDes, 0, &spsData, &spsSize, &spsCount, 0);
- OSStatus status2 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDes, 1, &ppsData, &ppsSize, &ppsCount, 0);
-
- if (status1 == noErr & status2 == 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;
- //将数据复制到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;
- }
- //循环获取NAL Unit数据
- size_t offet = 0;
- //返回的NAL Unit数据前四个字节不是系统端的startCode(0001)
- //而是大端模式的帧长度
- const int lengthStartCode = 4;
- const int lengthBigFrame = 4;
- while (offet < totalLength - lengthBigFrame) {
- //获取NAL Unit数据长度
- uint32_t lengthNALU = 0;
- memcpy(&lengthNALU, dataPointerOut + offet, lengthBigFrame);
- lengthNALU = CFSwapInt32BigToHost(lengthBigFrame);//大端转系统端
- //获取到编码好的视频startCode + NAL Uint
- NSMutableData *data = [NSMutableData dataWithCapacity:lengthStartCode + lengthNALU];
- [data appendBytes:startCode length:lengthStartCode];
- [data appendBytes:dataPointerOut + offet + lengthBigFrame length:lengthNALU];
-
- dispatch_async(encoder.encodeQueue, ^{
- if ([encoder.delegate respondsToSelector:@selector(encodeVideoCallback:)]) {
- [encoder.delegate encodeVideoCallback:data];
- }
- });
- offet += lengthStartCode + lengthNALU;
- }
- }
CMSampleBufferRef
获取到当前帧的相关属性,判断是否是关键帧。编码前后的视频数据都是CMSampleBufferRef
类型。sps、pps
。sps、pps
sps、pps
拼接到NSData
中,并且开头加上startCode(00 00 00 01)
,NAL Unit
之间使用startCode(00 00 00 01)
进行分割的。NSData
格式的sps、pps
回调出去。dataPointerOut
CMSampleBufferGetDataBuffer
:从CMSampleBufferRef
中获取CMBlockBufferRef
。CMBlockBufferGetDataPointer
:将数据复制到dataPointerOut
,获取到数据的总长度。NAL Unit
数据。memcpy
:获取NAL Unit
数据长度。startCode(00 00 00 01)
。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] & 0x1F);
- //0 01 00111 & 39
- //0 00 11111 31
- //0 00 00111 7
- //NSLog(@"type: %hhu, %d", frame[4], type);
-
- //将NAL Unit开始码转为4字节大端NAL Unit的长度信息。
- uint32_t naluSize = lengthFrame - 4;
- uint8_t *pNaluSize = (uint8_t *)(&naluSize);
- 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://sps
- self->_spsSize = naluSize;
- self->_sps = malloc(self->_spsSize);
- memcpy(self->_sps, &frameNALU[4], self->_spsSize);
- break;
- case 0x08://pps
- self->_ppsSize = naluSize;
- self->_pps = malloc(self->_ppsSize);
- memcpy(self->_pps, &frameNALU[4], self->_ppsSize);
- break;
- default://其他帧(0x01到 0x05)
- if ([self createDecoder]) {
- pixelBuffer = [self decodeNALUFrame:frameNALU withFrameLength:lengthFrame];
- }
- break;
- }
- });
- }
NSData
数据就是我门上面编码回调过来的编码后的视频数据。int type = (frameNALU[4] & 0x1F);
:获取该数据类型。type
为7是sps
, 8是pps
。NAL Unit
数据的分割码也就是前面说的startCode(\x00\x00\x00\x01)
,所以这里取第5个字节的数据,frameNALU[4]
也就是NAL Unit
的header
。0 01 00111(frameNALU[4])
& 0 00 11111(0x1F)
= 0 00 00111
转为十进制就是7
代表sps
。header
分为三部分0-01-00111
:0
代表 禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法。01
用来表示当前NAL单元的优先级。非0值表示参考字段/帧/图片数据,其他不那么重要的数据则为0。对于非0值,值越大表示NAL Unit
重要性越高。00111
指定NAL Unit
类型,这就是为什么按位与运算用0 00 11111(0x1F)
。type
为0x05
时代表时I
帧(关键帧)这时我们需要先创建解码器。0x07
时创建sps
数据。0x08
时创建pps
数据。创建解码器:
- - (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, &_decoderDesc);
- 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, &decompressionCallback, &_decodeSesion);
- 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;
- }
CMVideoFormatDescriptionCreateFromH264ParameterSets
设置解码参数:kCFAllocatorDefault
使用默认的内存分配startCode
的长度 4VTDecompressionOutputCallbackRecord
解码回调设置。VTDecompressionSessionCreate
创建解码器。kCFAllocatorDefault
使用默认的内存分配。NULL
让video toolbox
选择解码器。NULL
无要求。VTSessionSetProperty
设置解码会话属性。解码:
- - (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, &blockBufferOut);
- if (status != kCMBlockBufferNoErr) {
- NSLog(@"CMBlockBufferCreateWithMemoryBlock error status:%d", (int)status);
- return outputPixelBuffer;
- }
-
- CMSampleBufferRef sampleBuffer = NULL;
- const size_t sampleSizeArray[] = {lengthFrame};
- //2.创建sampleBuffer
- status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBufferOut, _decoderDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
- 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, &outputPixelBuffer, &decodeInfoFlags);
- 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;
- }
CMBlockBufferCreateWithMemoryBlock
创建CMBlockBufferRef
:kCFAllocatorDefault
使用默认内存分配frameNALU
NULL
时用于分配内存块,参数2不为NULL
时用于释放内存块,kCFAllocatorNull
不需要释放内存块。blockAllocator
会被忽略)。如果 参数2内存块 为NULL
,则其Allocate()
必须为非NULL
。如果分配成功,将在分配内存块时调用一次Allocate
。释放CMBlockBuffer
时将调用Free()
。CMBlockBuffer
地址,不能为空。CMSampleBufferCreateReady
创建CMSampleBufferRef
:kCFAllocatorDefault
使用默认内存分配blockBufferOut
.不能为NULLCMSampleBuffer
个数.numSamples
numSamples
, 默认为1CMSampleBufferRef
对象VTDecompressionSessionDecodeFrame
解码:CMSampleBufferRef
对象,包含一个或多个视频帧。CVPixelBufferRef
。解码成功后回调函数:
- 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
回调出去用来展示。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。