音视频-视频编/解码 实战

2020-12-10  本文已影响0人  Maji1

本文主要介绍如何利用 VideoToolBox 实现对视频的硬编/解码。

先来简单看下 音视频的采集

一、音视频的采集

音视频采集的核心流程: 音/视频采集
- (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;
}

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);
    });
}

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;
    }
}

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;
        }
    });
}

创建解码器:

- (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;
}

解码:

- (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;
}

解码成功后回调函数:

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);
    });
    
}
上一篇 下一篇

猜你喜欢

热点阅读