iOS视频编码实战VideoToolbox

news2024/11/17 9:36:45

需求

iOS中编码视频数据,一般情况而言一个项目仅需要一个编码器,不过有时特殊需求可能需要两个编码器同时工作.本例中实现了编码器类.仅通过指定不同编码器的枚举值就可以快速生成需要的编码器,且支持两个编码器一起工作.


实现原理:

iOS中利用VideoToolBox框架完成视频硬编码操作,支持H.264,H.265编码器.

软编码:使用CPU进行编码。

硬编码:不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。


测试结果

本例通过将编码后的文件写成.mov文件, 来测试h264, h265编码效率, 录制时间相同,场景基本相同,结果显示h265仅需要h264一半的内存就可以完成同样的画质.注意,录制出来的文件只能用ffmpeg相关工具播放.

实现步骤

1. 初始化编码器参数

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

  

本例中的编码器类不是单例,因为我们可以生成出h264编码器,h265编码器,以及让生成两个不同类型编码器对象同时工作.这里指定的宽高帧率需要与相机保持一致. 比特率即播放过程中平均码率,是否支持实时编码,如果支持实时编码码率则无法控制.最后我们仅仅可以通过指定编码器的类型来决定创建h264编码器还是h265编码器.

  • 判断是否支持编码器

判断是否支持hevc编码器,并不是所有的设备都支持h265编码器,这由硬件决定,但是没有直接的API去判断是否支持h265编码器,在这里借助AVAssetExportPresetHEVCHighestQuality属性来间接判断是否支持h265编码.

注意: h265编码的软件API需要在iOS 11以上的操作系统才能使用. 目前所有流行的iPhone已都支持h264编码器.

    // You could select h264 / h265 encoder.
    self.videoEncoder = [[XDXVideoEncoder alloc] initWithWidth:1280
                                                        height:720
                                                           fps:30
                                                       bitrate:2048
                                       isSupportRealTimeEncode:NO
                                                   encoderType:XDXH265Encoder]; // XDXH264Encoder
                                                  
-(instancetype)initWithWidth:(int)width height:(int)height fps:(int)fps bitrate:(int)bitrate isSupportRealTimeEncode:(BOOL)isSupportRealTimeEncode encoderType:(XDXVideoEncoderType)encoderType {
    if (self = [super init]) {
        mSession              = NULL;
        mVideoFile            = NULL;
        _width                = width;
        _height               = height;
        _fps                  = fps;
        _bitrate              = bitrate << 10;  //convert to bps
        _errorCount           = 0;
        _isSupportEncoder     = NO;
        _encoderType          = encoderType;
        _lock                 = [[NSLock alloc] init];
        _isSupportRealTimeEncode = isSupportRealTimeEncode;
        _needResetKeyParamSetBuffer = YES;
        if (encoderType == XDXH265Encoder) {
            if (@available(iOS 11.0, *)) {
                if ([[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality]) {
                    _isSupportEncoder = YES;
                }
            }
        }else if (encoderType == XDXH264Encoder){
            _isSupportEncoder = YES;
        }
        
        log4cplus_info("Video Encoder:","Init encoder width:%d, height:%d, fps:%d, bitrate:%d, is support encoder:%d, encoder type:H%lu", width, height, fps, bitrate, isSupportRealTimeEncode, (unsigned long)encoderType);
    }
    
    return self;
}

2. 初始化编码器

初始化一个编码器分为以下三个步骤, 首先新建一个VTCompressionSessionRef引用对象管理编码器, 然后将编码器所有属性赋值给该对象.最后在编码前预先分配一些资源(即为要编码的数据预先分配内存)以便编码buffer使用.

- (void)configureEncoderWithWidth:(int)width height:(int)height {
    log4cplus_info("Video Encoder:", "configure encoder with and height for init,with = %d,height = %d",width, height);
    
    if(width == 0 || height == 0) {
        log4cplus_error("Video Encoder:", "encoder param can't is null. width:%d, height:%d",width, height);
        return;
    }
    
    self.width   = width;
    self.height  = height;
    
    mSession = [self configureEncoderWithEncoderType:self.encoderType
                                            callback:EncodeCallBack
                                               width:self.width
                                              height:self.height
                                                 fps:self.fps
                                             bitrate:self.bitrate
                             isSupportRealtimeEncode:self.isSupportRealTimeEncode
                                      iFrameDuration:30
                                                lock:self.lock];
}
​
- (VTCompressionSessionRef)configureEncoderWithEncoderType:(XDXVideoEncoderType)encoderType callback:(VTCompressionOutputCallback)callback width:(int)width height:(int)height fps:(int)fps bitrate:(int)bitrate isSupportRealtimeEncode:(BOOL)isSupportRealtimeEncode iFrameDuration:(int)iFrameDuration lock:(NSLock *)lock {
    log4cplus_info("Video Encoder:","configure encoder width:%d, height:%d, fps:%d, bitrate:%d, is support realtime encode:%d, I frame duration:%d", width, height, fps, bitrate, isSupportRealtimeEncode, iFrameDuration);
    
    [lock lock];
    // Create compression session
    VTCompressionSessionRef session = [self createCompressionSessionWithEncoderType:encoderType
                                                                              width:width
                                                                             height:height
                                                                           callback:callback];
    
    // Set compresssion property
    [self setCompressionSessionPropertyWithSession:session
                                               fps:fps
                                           bitrate:bitrate
                           isSupportRealtimeEncode:isSupportRealtimeEncode
                                    iFrameDuration:iFrameDuration
                                       EncoderType:encoderType];
    
    // Prepare to encode
    OSStatus status = VTCompressionSessionPrepareToEncodeFrames(session);
    [lock unlock];
    if(status != noErr) {
        log4cplus_error("Video Encoder:", "create encoder failed, status: %d",(int)status);
        return NULL;
    }else {
        log4cplus_info("Video Encoder:","create encoder success");
        return session;
    }
}

2.1. 创建VTCompressionSessionRef对象

VTCompressionSessionCreate: 创建视频编码器session, 即管理编码器上下文的对象.

  • allocator: session的内存分配器.传递NULL表示默认的分配器.

  • width,height: 指定编码器的像素的宽高,与捕捉到的视频分辨率保持一致

  • codecType: 编码器类型.目前可用h264, h265两种主流编码器,h264应用最为广泛.h265编码器是h264的下一代,压缩性能更高,不过刚在iOS11中开放出来,存在一些bug.

  • encoderSpecification: 指定必须使用特定的编码器.一般传NULL即可.video toolbox会自己选择.

  • sourceImageBufferAttributes: 原始视频数据需要的属性.主要用于创建a pixel buffer pool.

  • compressedDataAllocator: 压缩数据的内存分配器.传NULL表示使用默认的分配器.

  • outputCallback: 接收压缩数据的回调.这个回调可以选择使用同步或异步方式接收.如果用同步则与VTCompressionSessionEncodeFrame函数线程保持一致,如果用异步会新建一条线程接收.该参数也可传NULL不过当且仅当我们使用VTCompressionSessionEncodeFrameWithOutputHandler函数作编码时.

  • outputCallbackRefCon: 可以传入用户自定义数据.主要用于回调函数与主类之间的交互.

  • compressionSessionOut: 传入要创建的session的内存地址.注意,session不能为NULL.

VT_EXPORT OSStatus 
VTCompressionSessionCreate(
    CM_NULLABLE CFAllocatorRef                          allocator,
    int32_t                                             width,
    int32_t                                             height,
    CMVideoCodecType                                    codecType,
    CM_NULLABLE CFDictionaryRef                         encoderSpecification,
    CM_NULLABLE CFDictionaryRef                         sourceImageBufferAttributes,
    CM_NULLABLE CFAllocatorRef                          compressedDataAllocator,
    CM_NULLABLE VTCompressionOutputCallback             outputCallback,
    void * CM_NULLABLE                                  outputCallbackRefCon,
    CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

下面是具体用法.注意如果相机采集的分辨率改变,需要销毁当前编码器session重新创建.

- (VTCompressionSessionRef)createCompressionSessionWithEncoderType:(XDXVideoEncoderType)encoderType width:(int)width height:(int)height callback:(VTCompressionOutputCallback)callback {
    CMVideoCodecType codecType;
    if (encoderType == XDXH264Encoder) {
        codecType = kCMVideoCodecType_H264;
    }else if (encoderType == XDXH265Encoder) {
        codecType = kCMVideoCodecType_HEVC;
    }else {
        return nil;
    }
    
    VTCompressionSessionRef session;
    OSStatus status = VTCompressionSessionCreate(NULL,
                                                 width,
                                                 height,
                                                 codecType,
                                                 NULL,
                                                 NULL,
                                                 NULL,
                                                 callback,
                                                 (__bridge void *)self,
                                                 &session);
    
    if (status != noErr) {
        log4cplus_error("Video Encoder:", "%s: Create session failed:%d",__func__,(int)status);
        return nil;
    }else {
        return session;
    }
}

2.2. 设置session属性

  • 查询session是否支持当前属性

创建好session后,调用VTSessionCopySupportedPropertyDictionary函数可以将当前session支持的所有属性拷贝到指定的字典中,以后在设置属性前先在字典中查询是否支持即可.

- (BOOL)isSupportPropertyWithSession:(VTCompressionSessionRef)session key:(CFStringRef)key {
    OSStatus status;
    static CFDictionaryRef supportedPropertyDictionary;
    if (!supportedPropertyDictionary) {
        status = VTSessionCopySupportedPropertyDictionary(session, &supportedPropertyDictionary);
        if (status != noErr) {
            return NO;
        }
    }
    
    BOOL isSupport = [NSNumber numberWithBool:CFDictionaryContainsKey(supportedPropertyDictionary, key)].intValue;
    return isSupport;
}
  • 设置session的属性

使用VTSessionSetProperty函数指定key, value即可设置属性.

- (OSStatus)setSessionPropertyWithSession:(VTCompressionSessionRef)session key:(CFStringRef)key value:(CFTypeRef)value {
    if (value == nil || value == NULL || value == 0x0) {
        return noErr;
    }
    
    OSStatus status = VTSessionSetProperty(session, key, value);
    if (status != noErr)  {
        log4cplus_error("Video Encoder:", "Set session of %s Failed, status = %d",CFStringGetCStringPtr(key, kCFStringEncodingUTF8),status);
    }
    return status;
}
  • kVTCompressionPropertyKey_MaxFrameDelayCount: 编码器在输出压缩帧前允许保留的最大帧数.默认为kVTUnlimitedFrameDelayCount,即不限制保留帧数.比如当前要编码10帧数据,最大延迟帧数为3(M), 那么在编码10(N)帧视频数据时,10-3(N-M)帧数据必须已经发送给编码回调.即已经编好了N-M帧数据,还保留M帧未编码的数据.

  • kVTCompressionPropertyKey_ExpectedFrameRate: 期望帧率,帧率以每秒钟接收的视频帧数量来衡量.此属性无法控制帧率而仅仅作为编码器编码的指示.以便在编码前设置内部配置.实际取决于视频帧的duration并且可能是不同的.默认是0,表示未知.

  • kVTCompressionPropertyKey_AverageBitRate: 长期编码的平均码率.此属性不是一个绝对设置,实际产生的码率可能高于此值.默认为0,表示编码器应该自行决定编码数据的大小.注意,码率设置仅在为原始帧提供定时信息时有效,并且某些编解码器不支持限制到指定的码率。

  • kVTCompressionPropertyKey_DataRateLimits: 可以选择两个以下的硬性限制对于码率.每个硬限制由以字节为单位的数据大小和以秒为单位的持续时间来描述,并要求该持续时间(在解码时间内)的任何连续段的压缩数据的总大小不得超过数据大小。默认情况下,不设置数据速率限制。该属性是偶数个CFNumber的CFArray,在字节和秒之间交替。请注意,数据速率设置仅在为原始帧提供定时信息时有效,并且某些编解码器不支持限制指定的数据速率。

  • kVTCompressionPropertyKey_RealTime: 是否实时执行压缩.false表示视频编码器可以比实时更慢地工作,以产生更好的结果.设置为true可以更加及时的编码.默认为NULL,表示未知.

  • kVTCompressionPropertyKey_AllowFrameReordering: 如果编码器开启B帧,则时间会乱序,编码器必须重新排序.默认为True,将其设置为false以防止帧重新排序.注意: iOS中一般不用相机采集B帧.

  • kVTCompressionPropertyKey_ProfileLevel: 指定编码比特流的配置文件和级别。可用的配置文件和级别因格式和视频编码器而异。视频编码器应该在可用的地方使用标准密钥,而不是标准模式。

  • kVTCompressionPropertyKey_H264EntropyMode: H.264压缩的熵编码模式。如果H.264编码器支持,则此属性控制编码器是使用基于上下文的自适应可变长度编码(CAVLC)还是基于上下文的自适应二进制算术编码(CABAC)。CABAC通常以更高的计算开销为代价提供更好的压缩。默认值是编码器特定的,可能会根据其他编码器设置而改变。使用此属性时应小心 - 更改可能会导致配置与请求的配置文件和级别不兼容。这种情况下的结果是未定义的,可能包括编码错误或不符合要求的输出流。

  • kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration: 从一个关键帧到下一个关键帧的最长持续时间(秒)。默认为零,没有限制。当帧速率可变时,此属性特别有用。此键可以与kVTCompressionPropertyKey\_MaxKeyFrameInterval一起设置,并且将强制执行这两个限制 - 每X帧或每Y秒需要一个关键帧,以先到者为准。

  • kVTCompressionPropertyKey_MaxKeyFrameInterval: 关键帧之间的最大间隔,以帧的数量为单位。关键帧,也称为I帧,重置帧间依赖关系;解码关键帧足以准备解码器以正确解码随后的差异帧。允许视频编码器更频繁地生成关键帧,如果这将导致更有效的压缩。默认关键帧间隔为0,表示视频编码器应选择放置所有关键帧的位置。关键帧间隔为1表示每帧必须是关键帧,2表示至少每隔一帧必须是关键帧等此键可以与kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration一起设置,并且将强制执行这两个限制 - 每X帧或每Y秒需要一个关键帧,以先到者为准。

    // Set compresssion property
    [self setCompressionSessionPropertyWithSession:session
                                               fps:fps
                                           bitrate:bitrate
                           isSupportRealtimeEncode:isSupportRealtimeEncode
                                    iFrameDuration:iFrameDuration
                                       EncoderType:encoderType];
​
- (void)setCompressionSessionPropertyWithSession:(VTCompressionSessionRef)session fps:(int)fps bitrate:(int)bitrate isSupportRealtimeEncode:(BOOL)isSupportRealtimeEncode iFrameDuration:(int)iFrameDuration EncoderType:(XDXVideoEncoderType)encoderType {
    
    int maxCount = 3;
    if (!isSupportRealtimeEncode) {
        if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_MaxFrameDelayCount]) {
            CFNumberRef ref   = CFNumberCreate(NULL, kCFNumberSInt32Type, &maxCount);
            [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_MaxFrameDelayCount value:ref];
            CFRelease(ref);
        }
    }
    
    if(fps) {
        if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_ExpectedFrameRate]) {
            int         value = fps;
            CFNumberRef ref   = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
            [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_ExpectedFrameRate value:ref];
            CFRelease(ref);
        }
    }else {
        log4cplus_error("Video Encoder:", "Current fps is 0");
        return;
    }
    
    if(bitrate) {
        if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_AverageBitRate]) {
            int value = bitrate << 10;
            CFNumberRef ref = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
            [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_AverageBitRate value:ref];
            CFRelease(ref);
        }
    }else {
        log4cplus_error("Video Encoder:", "Current bitrate is 0");
        return;
    }
    
    
    if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_RealTime]) {
        log4cplus_info("Video Encoder:", "use realTimeEncoder");
        [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_RealTime value:isSupportRealtimeEncode ? kCFBooleanTrue : kCFBooleanFalse];
    }
    
    // Ban B frame.
    if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_AllowFrameReordering]) {
        [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_AllowFrameReordering value:kCFBooleanFalse];
    }
    
    if (encoderType == XDXH264Encoder) {
        if (isSupportRealtimeEncode) {
            if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel]) {
                [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel value:kVTProfileLevel_H264_Main_AutoLevel];
            }
        }else {
            if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel]) {
                [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel value:kVTProfileLevel_H264_Baseline_AutoLevel];
            }
            
            if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_H264EntropyMode]) {
                [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_H264EntropyMode value:kVTH264EntropyMode_CAVLC];
            }
        }
    }else if (encoderType == XDXH265Encoder) {
        if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel]) {
            [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_ProfileLevel value:kVTProfileLevel_HEVC_Main_AutoLevel];
        }
    }
    
    
    if([self isSupportPropertyWithSession:session key:kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration]) {
        int         value   = iFrameDuration;
        CFNumberRef ref     = CFNumberCreate(NULL, kCFNumberSInt32Type, &value);
        [self setSessionPropertyWithSession:session key:kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration value:ref];
        CFRelease(ref);
    }
    
    log4cplus_info("Video Encoder:", "The compression session max frame delay count = %d, expected frame rate = %d, average bitrate = %d, is support realtime encode = %d, I frame duration = %d",maxCount, fps, bitrate, isSupportRealtimeEncode,iFrameDuration);
}

2.3. 编码前资源分配

您可以选择调用此函数,以便为编码器提供在开始编码帧之前执行任何必要资源分配的机会。此可选调用可用于为编码器提供在开始编码帧之前分配所需资源的机会。如果未调用此方法,则将在第一个VTCompressionSessionEncodeFrame调用上分配任何必要的资源。额外调用此函数将不起作用。

    // Prepare to encode
    OSStatus status = VTCompressionSessionPrepareToEncodeFrames(session);
    [lock unlock];
    if(status != noErr) {
        log4cplus_error("Video Encoder:", "create encoder failed, status: %d",(int)status);
        return NULL;
    }else {
        log4cplus_info("Video Encoder:","create encoder success");
        return session;
    }

执行到这里,初始化编码器的工作已经做完,接下来我们需要将视频帧数据进行编码. 本例中使用AVCaptureSession采集视频帧以传给编码器编码.

3.编码

注意,因为编码线程与创建,销毁编码器过程属于异步操作,所以需要加锁.

  • 时间戳同步

首先我们取第一帧视频数据为基准点,取系统当前时间,作为编码第一帧数据的基准时间. 此操作主要用于后期的音视频同步,本例中不作过多说明,另外,时间戳同步生成机制也不像本例中这么简单.可以自行制定生成规则.

  • 时间戳校正

判断当前编码的视频帧中的时间戳是否大于前一帧, 因为视频是严格按时间戳排序播放的,所以时间戳应该是一直递增的,但是考虑到传给编码器的可能不是一个视频源,比如一开始是摄像头采集的,后面换成从网络流解码的视频原始数据,此时时间戳必定不同步,如果强行将其传给编码器,则画面会出现卡顿.

  • 编码视频帧

    • session: 先前配置好的session

    • imageBuffer: 原始视频数据

    • presentationTimeStamp: 视频帧的pts

    • duration: 此帧的持续时间,将附加到样本缓冲区。如果没有持续时间信息,传kCMTimeInvalid。

    • frameProperties: 指定视频帧的其他属性,这里以是否强制产生I帧为例.

    • sourceFrameRefcon: 可以传递给回调函数原始帧的引用.

    • infoFlagsOut: 指向VTEncodeInfoFlags以接收有关编码操作的信息。如果编码是(或正在)异步运行,则可以设置kVTEncodeInfo_Asynchronous位。如果帧被丢弃(同步),则可以设置kVTEncodeInfo_FrameDropped位。如果您不想接收此信息,请传递NULL。

VT_EXPORT OSStatus
VTCompressionSessionEncodeFrame(
    CM_NONNULL VTCompressionSessionRef  session,
    CM_NONNULL CVImageBufferRef         imageBuffer,
    CMTime                              presentationTimeStamp,
    CMTime                              duration, // may be kCMTimeInvalid
    CM_NULLABLE CFDictionaryRef         frameProperties,
    void * CM_NULLABLE                  sourceFrameRefcon,
    VTEncodeInfoFlags * CM_NULLABLE     infoFlagsOut ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

-(void)startEncodeWithBuffer:(CMSampleBufferRef)sampleBuffer session:(VTCompressionSessionRef)session isNeedFreeBuffer:(BOOL)isNeedFreeBuffer isDrop:(BOOL)isDrop  needForceInsertKeyFrame:(BOOL)needForceInsertKeyFrame lock:(NSLock *)lock {
    [lock lock];
    
    if(session == NULL) {
        log4cplus_error("Video Encoder:", "%s,session is empty",__func__);
        [self handleEncodeFailedWithIsNeedFreeBuffer:isNeedFreeBuffer sampleBuffer:sampleBuffer];
        return;
    }
    
    //the first frame must be iframe then create the reference timeStamp;
    static BOOL isFirstFrame = YES;
    if(isFirstFrame && g_capture_base_time == 0) {
        CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        g_capture_base_time = CMTimeGetSeconds(pts);// system absolutly time(s)
        //        g_capture_base_time = g_tvustartcaptureTime - (ntp_time_offset/1000);
        isFirstFrame = NO;
        log4cplus_error("Video Encoder:","start capture time = %u",g_capture_base_time);
    }
    
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    
    // Switch different source data will show mosaic because timestamp not sync.
    static int64_t lastPts = 0;
    int64_t currentPts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
    if (currentPts - lastPts < 0) {
        log4cplus_error("Video Encoder:","Switch different source data the timestamp < last timestamp, currentPts = %lld, lastPts = %lld, duration = %lld",currentPts, lastPts, currentPts - lastPts);
        [self handleEncodeFailedWithIsNeedFreeBuffer:isNeedFreeBuffer sampleBuffer:sampleBuffer];
        return;
    }
    lastPts = currentPts;
    
    OSStatus status = noErr;
    NSDictionary *properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame:@(needForceInsertKeyFrame)};
    status = VTCompressionSessionEncodeFrame(session,
                                             imageBuffer,
                                             presentationTimeStamp,
                                             kCMTimeInvalid,
                                             (__bridge CFDictionaryRef)properties,
                                             NULL,
                                             NULL);
    
    if(status != noErr) {
        log4cplus_error("Video Encoder:", "encode frame failed");
        [self handleEncodeFailedWithIsNeedFreeBuffer:isNeedFreeBuffer sampleBuffer:sampleBuffer];
    }
    
    [lock unlock];
    if (isNeedFreeBuffer) {
        if (sampleBuffer != NULL) {
            CFRelease(sampleBuffer);
            log4cplus_debug("Video Encoder:", "release the sample buffer");
        }
    }
}

4. h264码流 - H264, H265硬件编解码基础及码流分析

以下关于码流部分的代码如果看不懂,建议一定要先看下标题推荐的链接,里面是了解编解码器的基础知识以及iOS中VideoToolbox框架中数据结构的解析.

5. 回调函数

  • 排错校验

如果status中有错误信息,表示编码失败.可以做一些特殊处理.

  • 时间戳纠正

我们需要为编码后的数据填充时间戳,这里我们可以根据自己的规则制定一套时间戳生成规则,我们这里仅仅用最简单的偏移量,即用第一帧视频数据编码前系统时间为基准点,然后每帧编码后的时间取采集到的时间戳减去基准时间得到的值作为编码后数据的时间戳.

  • 寻找I帧.

原始视频数据经过编码后分为I帧,B帧,P帧.iOS端一般不开启B帧,B帧需要重新排序,我们拿到编码后的数据首先通过kCMSampleAttachmentKey_DependsOnOthers属性判断是否为I帧,如果是I帧,要从I帧中读取NALU头部关键信息,即vps,sps,pps. vps仅在h265编码器中才有.没有这些编码的视频无法在另一端播放,也无法录制成文件.

  • 读取编码器关键信息

从I帧中可以读取到vps,sps,pps数据具体的内容.如果是h264编码器调用CMVideoFormatDescriptionGetH264ParameterSetAtIndex函数,如果是h265编码器调用CMVideoFormatDescriptionGetHEVCParameterSetAtIndex函数,其中第二个参数的索引值0,1,2就分别代表这些数据的索引值.

找到这些数据后我们需要将它们拼接起来,因为它们是独立的NALU,即以0x00, 0x00, 0x00, 0x01作为隔断符以区分sps,pps.

所以,我们按照规则将拿到的vps,sps,pps中间分别以00 00 00 01作为隔断符以拼接成一个完整连续的buffer.本例以写文件为例,我们首先要将NALU头信息写入文件,也就是将I帧先写进去,因为I帧代表一个完整图像,P帧需要依赖I帧才能产生图像,所以我们文件的读取开头必须是一个I帧数据.

  • 一帧图片跟NALU的关联:

一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了。

注意:片(slice)的概念不同与帧(frame),帧(frame)是用作描述一张图片的,一帧(frame)对应一张图片,而片(slice),是 H.264 中提出的新概念,是通过编码图片后切分通过高效的方式整合出来的概念,一张图片至少有一个或多个片(slice)。片(slice)都是又 NALU 装载并进行网络传输的,但是这并不代表 NALU 内就一定是切片,这是充分不必要条件,因为 NALU 还有可能装载着其他用作描述视频的信息。

  • 分割码流中的NALU

首先通过CMBlockBufferGetDataPointer获取视频帧数据.该帧表示一段H264/H265码流,其中可能包含多个NALU,我们需要找出每个NALU并用00 00 00 01作为隔断符. 即while循环就是寻找码流中的NALU,因为裸流中不含有start code.我们要将start code拷贝进去.

CFSwapInt32BigToHost: 从h264编码的数据的大端模式(字节序)转系统端模式

static void EncodeCallBack(void *outputCallbackRefCon,void *souceFrameRefCon,OSStatus status,VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    XDXVideoEncoder *encoder = (__bridge XDXVideoEncoder*)outputCallbackRefCon;
    
    if(status != noErr) {
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"H264: vtCallBack failed with %@", error);
        log4cplus_error("TVUEncoder", "encode frame failured! %s" ,error.debugDescription.UTF8String);
        return;
    }
    
    if (!encoder.isSupportEncoder) {
        return;
    }
    
    CMBlockBufferRef block = CMSampleBufferGetDataBuffer(sampleBuffer);
    CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    CMTime dts = CMSampleBufferGetDecodeTimeStamp(sampleBuffer);
    
    // Use our define time. (the time is used to sync audio and video)
    int64_t ptsAfter = (int64_t)((CMTimeGetSeconds(pts) - g_capture_base_time) * 1000);
    int64_t dtsAfter = (int64_t)((CMTimeGetSeconds(dts) - g_capture_base_time) * 1000);
    dtsAfter = ptsAfter;
    
    /*sometimes relative dts is zero, provide a workground to restore 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;
        
        // Note: the NALU header will not change if video resolution not change.
        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 == XDXH264Encoder) {
                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);
                
                log4cplus_info("Video Encoder:", "H264 find IDR frame, spsSize : %zu, ppsSize : %zu",spsSize, ppsSize);
            }else if (encoder.encoderType == XDXH265Encoder) {
                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);
                log4cplus_info("Video Encoder:", "H265 find IDR frame, vpsSize : %zu, spsSize : %zu, ppsSize : %zu",vpsSize,spsSize, ppsSize);
            }
            
            encoder.needResetKeyParamSetBuffer = NO;
        }
        
        if (encoder.isNeedRecord) {
            if (encoder->mVideoFile == NULL) {
                [encoder initSaveVideoFile];
                log4cplus_info("Video Encoder:", "Start video record.");
            }
            
            fwrite(keyParameterSetBuffer, 1, keyParameterSetBufferSize, encoder->mVideoFile);
        }
        
        log4cplus_info("Video Encoder:", "Load a I frame.");
    }
    
    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;
    }
    
    if (encoder.isNeedRecord && encoder->mVideoFile != NULL) {
        fwrite(bufferDataPointer, 1, blockBufferLength, encoder->mVideoFile);
    }else {
        if (encoder->mVideoFile != NULL) {
            fclose(encoder->mVideoFile);
            encoder->mVideoFile = NULL;
            log4cplus_info("Video Encoder:", "Stop video record.");
        }
    }
    
//    log4cplus_debug("Video Encoder:","H265 encoded video:%lld, size:%lu, interval:%lld", dtsAfter,blockBufferLength, dtsAfter - last_dts);
    
    last_dts = dtsAfter;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/70647.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TS201的Flag输出状态控制LED亮灭原理和例程(含参考代码)

目的&#xff1a; 理解FLAG可编程作为输入输出引脚&#xff0c;并且能够利用按键进行相应FLAG&#xff08;FLAG0,FLAG1&#xff09;标志的输入来相应的FLAG标志(FLAG2,FLAG3)输出来控制与之相连的LED。掌握外部中断和定时器中断的设置以及其响应过程&#xff0c;理解外部硬件可…

hevc帧内planer预测模式和角度预测模式

帧内planer预测模式 planer预测模式适用于纹理相对平缓的图像区域&#xff0c;对于各个编码宏块而言&#xff0c;它不但能保持图像宏块边界良好的连续性。而且可以利用平面梯度信号随像素值的变化趋势而变化&#xff0c;在Planer预测模式下&#xff0c;可以将预测像素Px,y 看作…

企业应收账款管理存在的问题及对策

应收账款也就是信用交易&#xff0c;企业应收账款的产生是企业采取信用销售方式的必然结果。 现如今信用交易已经成为企业提高竞争力、扩大销售的必要手段&#xff0c;它充分挖掘和利用了企业的现有生产能力&#xff0c;扩大了销售量&#xff0c;增加了产品的市场份额&#xf…

潮玩积木国产化浪潮里,“中国积木”的自证之路

随着Z世代的崛起&#xff0c;潮玩从小众兴趣领域进入大众视野。 其中&#xff0c;作为年轻人喜爱的潮流品类之一&#xff0c;拼搭积木正在成为潮玩赛道的新风口。 哪怕疫情影响下&#xff0c;作为非必需消费品的积木仍然保持着中高速市场增长&#xff0c;足以被视为消费领域的…

[附源码]计算机毕业设计路政管理信息系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

ChatGPT入门指南

ChatGPT入门指南什么是ChatGPT&#xff1f;为什么ChatGPT意义重大&#xff1f;如何使用ChatGPT什么是ChatGPT&#xff1f; ChatGPT是基于聊天的生成预训练transformer模型的缩写&#xff0c;是一个强大的工具&#xff0c;可以以各种方式使用&#xff0c;以提高您在许多领域的生…

【Python】基础语法 3 (函数)

函数详解1. 函数是什么2. 语法格式3. 函数参数4. 函数返回值5. 变量作用域6. 函数执行过程7. 链式调用8. 嵌套调用9. 函数递归10. 参数默认值11. 关键字参数1. 函数是什么 编程中的函数和数学中的函数有一定的相似之处。 数学上的函数&#xff0c;比如 y sinx&#xff0c;x 取…

如何创建微信小程序?【创建小程序】

如何创建微信小程序呢&#xff1f;这是很多没有小程序的小伙伴经常问的问题&#xff0c;现在小程序给我们带来很多便利&#xff0c;而且很多企业公司也会有自己的小程序提供给他们的客户使用。那么创建微信小程序的步骤是什么呢&#xff0c;下面跟大家说说如何创建微信小程序。…

centos 模拟路由器功能实现内网和外网的联通

如下图的网络结构 192.168.65.128是一个windows主机&#xff0c;仅链接vmnet1的网卡。属于内网&#xff0c;无法连接外网&#xff0c;路由如下&#xff1a; IPv4 路由表 活动路由: 网络目标 网络掩码 网关 接口 跃点数 127.0.0.0 255.…

飞剪、追剪算法详细图解(附PLC完整源代码)

谈到运动控制就离不开编码器,有关编码器测速,测距的相关内容,大家可以查看专栏的其它文章,和飞剪控制息息相关的编码器测速,请参看下面的博客,链接如下: 如何通过编码器信号计算输送线/输送带线速度(飞剪、追剪算法基础)_RXXW_Dor的博客-CSDN博客不同品牌PLC如何采集…

Acrel-2000Z电力监控系统在重庆五桂堂历史文化商业街区的应用-Susie 周

1、项目概述 据悉原五桂堂街是一条上百年的地地道道的涪陵“老街”&#xff0c;北起火神庙&#xff08;后来的铁器社&#xff09;西侧&#xff0c;南止天主堂大门前横街尽头&#xff0c;长约200米&#xff0c;宽约2米&#xff0c;该项目建设地点位于重庆市涪陵区敦仁街道望栏桥…

设备树(Device Tree)

设备树介绍&#xff1a; 设备树是一个描述设备硬件资源的文件&#xff0c;该文件是由节点组成的树形结构。如下&#xff1a; / { node1 { a-string-property "A string"; a-string-list-property "first string", "second string"; // hex is …

Linux磁盘管理

Linux磁盘管理实验目的及要求1.熟悉Linux下磁盘的基本管理方法。2.了解Linux磁盘配额管理的意义和基本方法。3.掌握mount、fdisk、df等常用的磁盘管理命令。实验原理实验步骤1.使用不同的方法挂载/卸载磁盘并查看相关信息&#xff0c;具体步骤如下&#xff1a;1.挂载一个光驱或…

简述人工神经网络的定义,简述神经网络算法

1、人工智能十大算法 人工智能十大算法如下 线性回归&#xff08;Linear Regression&#xff09;可能是最流行的机器学习算法。线性回归就是要找一条直线&#xff0c;并且让这条直线尽可能地拟合散点图中的数据点。它试图通过将直线方程与该数据拟合来表示自变量&#xff08;x…

mapbox一学就会系列:01 第一个地图页面

文章目录前言一、mapbox是什么&#xff1f;官网官网示例效果尝鲜二、使用步骤1.引入mapbox-gl.js库在线库npm 形式安装2.使用方法无账号则申请&#xff0c;有账号则登录申请完成后&#xff0c;获取token创建一个地图元素容器使用token并配置创建一个地图示例效果总结前言 最近…

PMP项目管理证书有用么?什么人可以考呢?

有用&#xff0c;非常有用&#xff0c;PMP如今的价值在于越来越多的招聘和公司都需要PMP证书&#xff0c;有需求就有价值。&#xff08;资料文末&#xff09; 需求分两个方面来说&#xff0c;一个是个人&#xff0c;一个是组织。 个人的用处&#xff1a; 项目管理几乎不限行业…

fasterxml jaskson的使用

fasterxml jaskson 的使用为啥要撰写这玩儿&#xff1f;解析json格式字符串判断是否是json格式字符串解析原理与解析函数如何使用该函数&#xff1f;为啥要撰写这玩儿&#xff1f; 由于SpringBoot的依赖默认使用fasterxml-jaskson&#xff08;可能是由于不想使用其他json处理包…

[附源码]Python计算机毕业设计Django疫苗及注射管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

员工账号管理,超市便利店烟酒茶叶服装门店零售手机收银软件APP

https://www.bilibili.com/video/BV1v841157e4/?vd_sourcebe5b336e3cce67dfc9122f3eaf7119ad我们的门店零售手机收银A P P&#xff0c;不限制用户数&#xff0c;也就是有多少零售收银员工&#xff0c;都可以给他们分配账号&#xff0c;并下载A P P登录使用。, 视频播放量 1、弹…

计算两个颜色相似度

1.计算两个颜色相似度的公式如下: 颜色QColor1(R1, G1, B1)转成h1,s1,v1 颜色QColor2(R2, G2, B2)转成h2,s2,v2 detah=h1-h2 detas=s1-s2 detav=v1-v2 len = qsrt(detah * detah + detas * detas + detav * detav) if (len > 1) len = 1.0 similarity = (1.0 - le…