美文网首页音视频
iOS 视频硬编码H264/H265

iOS 视频硬编码H264/H265

作者: 四叶帅 | 来源:发表于2022-07-22 13:11 被阅读0次

    视频采集

    视频采集部分,上一篇文章已经提及
    iOS视频采集

    视频编码

    首先初始化编码器

    - (void)initEncoder{
        //视频宽、高、帧率、码率
        int width = 1280, height = 720, FPS = 25, bitrate = 2048;
        
        //默认H264编码,H265编码需要iOS 11
        encoderType = kCMVideoCodecType_H264;
        
        //    if (@available(iOS 11.0, *)) {
        //        if ([[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality]){
        //            ///是否支持H265
        //            encoderType = kCMVideoCodecType_HEVC;
        //        }
        //    }
        
        lock = [[NSLock alloc] init];
        
        [lock lock];
        
        //创建session
        VTCompressionSessionRef session;
        OSStatus status = VTCompressionSessionCreate(NULL,
                                                     width,
                                                     height,
                                                     encoderType,
                                                     NULL,
                                                     NULL,
                                                     NULL,
                                                     videoEncoderCallBack,
                                                     (__bridge void*)self,
                                                     &session);
        if (status == noErr) {
            
        }else{
            NSLog(@"创建session失败");
        }
        
        ///设置session属性
        // 设置实时编码输出(避免延迟)
        status = VTSessionSetProperty(session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        
        if (encoderType == kCMVideoCodecType_H264) {
            if ((YES)/*支持实时编码*/) {
                status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
            }else{
                //表示使用H264的Profile规格,可以设置Hight的AutoLevel规格
                status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
                //若是支持h264该属性设置编码器是否应该使用基于CAVLC 或是 CABAC
                status = VTSessionSetProperty(session, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CAVLC);
            }
            
        }else if (encoderType == kCMVideoCodecType_HEVC){
            ///H265
            status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_HEVC_Main_AutoLevel);
        }
        
        // 设置关键帧(GOPsize)间隔
        int frameInterval = 10;
        CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
        status = VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
        
        // 设置期望帧率
        CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &FPS);
        status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
        
        //设置码率,上限,单位是bps
        int bitRate = width * height * 3 * 4 * 8;
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
        status =  VTSessionSetProperty(session, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
        
        //设置码率,均值,单位是byte
        int bitRateLimit = width * height * 3 * 4;
        CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
        status = VTSessionSetProperty(session, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
        
        //告诉编码器开始编码
        status = VTCompressionSessionPrepareToEncodeFrames(session);
        
        [lock unlock];
        
        if (status != noErr) {
            NSLog(@"失败,需要排查问题和参数");
        }else{
            videoSession = session;
        }
        
    }
    

    传入需要编码的数据

    /// 传入需要编码的数据进行编码
    /// @param sampleBuffer 需要编码的原始数据
    /// @param isNeedFreeBuffer 是否需要释放,如果自己组装的,需要手动释放,系统返回的不需要
    - (void)startEncodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer isNeedFreeBuffer:(BOOL)isNeedFreeBuffer{
        [lock lock];
        //第一帧必须是iframe,然后创建引用的时间戳
        static BOOL isFirstFrame = YES;
        if (isFirstFrame && encode_capture_base_time == 0) {
            CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
            encode_capture_base_time = CMTimeGetSeconds(pts);
            isFirstFrame = NO;
        }
        
        CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        // 切换不同源数据会显示马赛克,因为时间戳不同步
        static int64_t lastPts = 0;
        int64_t currentPts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
        lastPts = currentPts;
        
        OSStatus status = noErr;
        
        //如果编码中途进行其他操作,这时候强制添加关键帧
        BOOL needForceInsertKeyFrame = NO;//是否需要强制插入关键帧
        NSDictionary *properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame:@(needForceInsertKeyFrame)};
        //编码
        status = VTCompressionSessionEncodeFrame(videoSession,
                                                 imageBuffer,
                                                 presentationTimeStamp,
                                                 kCMTimeInvalid,
                                                 (__bridge CFDictionaryRef)properties,
                                                 NULL,
                                                 NULL);
        if (status != noErr) {
            [self handleEncodeFailedWithIsNeedFreeBuffer:isNeedFreeBuffer sampleBuffer:sampleBuffer];
        }
        [lock unlock];
        
        if (isNeedFreeBuffer) {
            if (sampleBuffer != NULL) {
                CFRelease(sampleBuffer);
            }
        }
    }
    

    编码回调

    #pragma mark - callback -
    static void videoEncoderCallBack(void *outputCallbackRefCon,
                                     void *souceFrameRefCon,
                                     OSStatus status,
                                     VTEncodeInfoFlags infoFlags,
                                     CMSampleBufferRef sampleBuffer) {
        (当前类)*encoder = (__bridge (当前类)*)outputCallbackRefCon;
        if (status != noErr) {
            NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
            NSLog(@"编码返回失败 %@", error);
            return;
        }
        CMBlockBufferRef block = CMSampleBufferGetDataBuffer(sampleBuffer);
        CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        CMTime dts = CMSampleBufferGetDecodeTimeStamp(sampleBuffer);
        
        // 利用我们定义的时间。(此时间用于同步音频和视频)
        int64_t ptsAfter = (int64_t)((CMTimeGetSeconds(pts) - encode_capture_base_time) * 1000);
        int64_t dtsAfter = (int64_t)((CMTimeGetSeconds(dts) - encode_capture_base_time) * 1000);
        dtsAfter = ptsAfter;
        
        /*有时相对DTS为零,提供一个恢复DTS的工作区*/
        static int64_t last_dts = 0;
        if(dtsAfter == 0){
            dtsAfter = last_dts +33;
        }else if (dtsAfter == last_dts){
            dtsAfter = dtsAfter + 1;
        }
        
        
        BOOL isKeyFrame = NO;
        CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
        if(attachments != NULL) {
            CFDictionaryRef attachment =(CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
            CFBooleanRef dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
            isKeyFrame = (dependsOnOthers == kCFBooleanFalse);
        }
        
        if(isKeyFrame) {
            static uint8_t *keyParameterSetBuffer    = NULL;
            static size_t  keyParameterSetBufferSize = 0;
            
            // 注意:如果视频分辨率不改变,NALU头不会改变。
            if (keyParameterSetBufferSize == 0 || YES == encoder->needResetKeyParamSetBuffer) {
                const uint8_t  *vps, *sps, *pps;
                size_t         vpsSize, spsSize, ppsSize;
                int            NALUnitHeaderLengthOut;
                size_t         parmCount;
                
                if (keyParameterSetBuffer != NULL) {
                    free(keyParameterSetBuffer);
                }
                
                CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
                if (encoder->encoderType == kCMVideoCodecType_H264) {
                    CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, &NALUnitHeaderLengthOut);
                    CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, &NALUnitHeaderLengthOut);
                    
                    keyParameterSetBufferSize = spsSize+4+ppsSize+4;
                    keyParameterSetBuffer = (uint8_t*)malloc(keyParameterSetBufferSize);
                    memcpy(keyParameterSetBuffer, "\x00\x00\x00\x01", 4);
                    memcpy(&keyParameterSetBuffer[4], sps, spsSize);
                    memcpy(&keyParameterSetBuffer[4+spsSize], "\x00\x00\x00\x01", 4);
                    memcpy(&keyParameterSetBuffer[4+spsSize+4], pps, ppsSize);
                    
                    NSLog(@ "H264 find IDR frame, spsSize : %zu, ppsSize : %zu",spsSize, ppsSize);
                }else if (encoder->encoderType == kCMVideoCodecType_HEVC) {
                    CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vps, &vpsSize, &parmCount, &NALUnitHeaderLengthOut);
                    CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sps, &spsSize, &parmCount, &NALUnitHeaderLengthOut);
                    CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pps, &ppsSize, &parmCount, &NALUnitHeaderLengthOut);
                    
                    keyParameterSetBufferSize = vpsSize+4+spsSize+4+ppsSize+4;
                    keyParameterSetBuffer = (uint8_t*)malloc(keyParameterSetBufferSize);
                    memcpy(keyParameterSetBuffer, "\x00\x00\x00\x01", 4);
                    memcpy(&keyParameterSetBuffer[4], vps, vpsSize);
                    memcpy(&keyParameterSetBuffer[4+vpsSize], "\x00\x00\x00\x01", 4);
                    memcpy(&keyParameterSetBuffer[4+vpsSize+4], sps, spsSize);
                    memcpy(&keyParameterSetBuffer[4+vpsSize+4+spsSize], "\x00\x00\x00\x01", 4);
                    memcpy(&keyParameterSetBuffer[4+vpsSize+4+spsSize+4], pps, ppsSize);
                    NSLog(@ "H265 find IDR frame, vpsSize : %zu, spsSize : %zu, ppsSize : %zu",vpsSize,spsSize, ppsSize);
                }
                
                encoder->needResetKeyParamSetBuffer = NO;
            }
            //        是否是关键帧 NO
            //        是否是额外数据 YES
            //        keyParameterSetBuffer 就是编码后的数据
            //        keyParameterSetBufferSize 就是编码后数据的size
            //        dtsAfter timeStamp
            
            //        回调数据的地方 (2-1)
            NSLog(@"视频编码,加载 I 帧");
        }
        
        size_t   blockBufferLength;
        uint8_t  *bufferDataPointer = NULL;
        CMBlockBufferGetDataPointer(block, 0, NULL, &blockBufferLength, (char **)&bufferDataPointer);
        
        size_t bufferOffset = 0;
        while (bufferOffset < blockBufferLength - kStartCodeLength)
        {
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, bufferDataPointer+bufferOffset, kStartCodeLength);
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            memcpy(bufferDataPointer+bufferOffset, kStartCode, kStartCodeLength);
            bufferOffset += kStartCodeLength + NALUnitLength;
        }
        //    是否是关键帧 isKeyFrame
        //    是否是额外数据 NO
        //    bufferDataPointer 就是编码后的数据
        //    blockBufferLength 就是编码后数据的size
        //    dtsAfter timeStamp
        
        //    回调数据的地方 (2-2)
        
        last_dts = dtsAfter;
    }
    

    其他参数:

    {
        VTCompressionSessionRef videoSession;
        NSLock *lock;
        ///是否需要重置
        BOOL needResetKeyParamSetBuffer;
        ///编码方式
        CMVideoCodecType encoderType;
    }
    static const size_t  kStartCodeLength           = 4;
    static const uint8_t kStartCode[]               = {0x00, 0x00, 0x00, 0x01};
    uint32_t             encode_capture_base_time   = 0;
    

    Demo地址整理后奉上。
    有其他不明白的,可以留言,看到就会回复。
    如果喜欢,请帮忙点赞。支持转载,转载请附原文链接。

    相关文章

      网友评论

        本文标题:iOS 视频硬编码H264/H265

        本文链接:https://www.haomeiwen.com/subject/clsuirtx.html