音视频

iOS 视频硬编码H264/H265

2022-07-22  本文已影响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地址整理后奉上。
有其他不明白的,可以留言,看到就会回复。
如果喜欢,请帮忙点赞。支持转载,转载请附原文链接。

上一篇下一篇

猜你喜欢

热点阅读