VideoToolBox H264 硬编码

一. 主要函数说明

  • 创建会话
    使用VTCompressionSessionCreate()来创建会话。
 VTCompressionSessionCreate(
     //  指定的分配器,若设置为NULL,表示使用默认的分配器
     CFAllocatorRef  _Nullable allocator, 
     //  视频图像的宽                              
     int32_t width, 
     //  视频图像的高
     int32_t height,
     //  编码类型
     CMVideoCodecType codecType,
     //  编码规范,若设置为NULL,表示由系统自行选择编码规范
     CFDictionaryRef  _Nullable encoderSpecification,
     //  源像素缓冲区属性,若设置为NULL,表示自己创建
     CFDictionaryRef  _Nullable sourceImageBufferAttributes,
     //  压缩数据的分配器,若设置为NULL,表示使用默认的分配器
     CFAllocatorRef  _Nullable compressedDataAllocator,
     //  回调函数的函数指针
     VTCompressionOutputCallback  _Nullable outputCallback,
     //  回调函数的引用数据,将会传递到回调函数中
     void * _Nullable outputCallbackRefCon,
     //  接受生成的会话的地址
     VTCompressionSessionRef  _Nullable * _Nonnull compressionSessionOut 
 )

这个函数有一个OSStatus类型的返回值,若返回noErr,则表示创建成功。

  • 设置编码会话的属性
    使用VTSessionSetProperty()来完成编码属性的设置。
 VTSessionSetProperty(
     VTSessionRef  _Nonnull session,    //  要设置的编码会话
     CFStringRef  _Nonnull propertyKey, //  属性的key
     CFTypeRef  _Nullable propertyValue //  属性的value
 )

这个函数也有一个OSStatus类型的返回值,若返回noErr,则表示属性设置成功。

  • 准备编码
    使用VTCompressionSessionPrepareToEncodeFrames()
 VTCompressionSessionPrepareToEncodeFrames(
     VTCompressionSessionRef  _Nonnull session  //  准备编码的会话
 )

同上,若此函数返回noErr,则表示执行成功。

  • 编码

使用VTCompressionSessionEncodeFrame()函数来进行编码操作。

 VTCompressionSessionEncodeFrame(
     VTCompressionSessionRef  _Nonnull session, //  执行编码的会话
     CVImageBufferRef  _Nonnull imageBuffer,    //  要进行编码的图像数据帧
     CMTime presentationTimeStamp,              //  该帧的展示时间戳,每一个时间戳必须大于前一个时间戳
     CMTime duration,                           //  该帧的持续时间,若没有持续时间,则传递kCMTimeInvalid
     CFDictionaryRef  _Nullable frameProperties,//  该帧的属性
     void * _Nullable sourceFrameRefcon,        //  该帧的引用数据,将会传递到回调函数中
     VTEncodeInfoFlags * _Nullable infoFlagsOut //  用于接受此次编码操作信息的地址
 )

返回结果同上,若此函数返回noErr,则表示编码成功。

  • 结束编码
  1. 使用VTCompressionSessionCompleteFrames()函数来强制完成所有未处理的帧。

    VTCompressionSessionCompleteFrames(
        VTCompressionSessionRef  _Nonnull session, //  执行此操作的会话
        CMTime completeUntilPresentationTimeStamp  //  完成帧编码的时间戳,若传递kCMTimeInvalid,则会处理完所有待处理的帧再返回
    )
    

    返回结果同上。

  2. 使用VTCompressionSessionInvalidate()函数来设置编码会话失效。

    VTCompressionSessionInvalidate(
    
        VTCompressionSessionRef  _Nonnull session  //  将要失效的编码会话
    
    )
    

    返回结果同上

  • 编码回调函数

VideoToolBox定义了一个VTCompressionOutputCallback类型的结构体,我们需要根据其定义声明一个函数来获取回调信息。

结构体如下:

typedef void (*VTCompressionOutputCallback)
    void * CM_NULLABLE outputCallbackRefCon,    //  创建会话时传入的引用数据
    void * CM_NULLABLE sourceFrameRefCon,       //  编码时传入的引用数据
    OSStatus status,                            //  编码的状态
    VTEncodeInfoFlags infoFlags,                //  有关编码的信息
    CM_NULLABLE CMSampleBufferRef sampleBuffer  //  编码后的结果
);

二. 编码流程

编码流程

三. 具体实现

1. 创建编码会话

int32_t width = 480;   //  视频图像的宽
int32_t height = 640;  //  视频图像的高
VTCompressionSessionRef encodeSesion;

OSStatus status = VTCompressionSessionCreate(
                        kCFAllocatorDefault,               //  这里我们使用默认的分配器
                        width, 
                        height, 
                        kCMVideoCodecType_H264,            //  H264编码模式
                        NULL,                              //  由系统自行选择编码规范
                        NULL,                              //  自己创建
                        NULL,                              //  使用默认的分配器
                        VideoEncodeCallback,               //  自己定义的回调函数名 
                        (__bridge void * _Nullable)(self), //  这里我们将self传递过去 
                        &encodeSesion
                        );

if (status != noErr) {
    NSLog(@"Session create failed. status=%d", (int)status);
}

2. 设置编码属性

常用的属性

  • kVTCompressionPropertyKey_RealTime

    这个属性表示是否实时编码,值为一个CFBoolean类型。

  • kVTCompressionPropertyKey_ProfileLevel

    这个属性表示编码的效率级别,一般传kVTProfileLevel_H264_Baseline_AutoLevel即可

  • kVTCompressionPropertyKey_AllowFrameReordering

    这个属性表示是否允许帧重新排序。如果对B帧进行重新编码,编码器必须对帧进行重新排序。所以这个属性可以间接理解为是否产生B帧。该属性的值为一个CFBoolean类型。

  • kVTCompressionPropertyKey_MaxKeyFrameInterval

    这个属性表示I帧的间隔,也就是GOP,这个值设置太大的话,图像会模糊。该属性的值为一个CFNumberRef类型

  • kVTCompressionPropertyKey_ExpectedFrameRate

    这个属性表示期望编码后的帧率,也就是FPS。这个设置并不能控制帧率,实际的帧率还依赖于帧的持续时间,并且有可能变化。该属性的值为一个CFNumberRef类型。

  • kVTCompressionPropertyKey_AverageBitRate

    这个属性表示平均码率,单位是bps。码率大的话,画面会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊。该属性的值为一个CFNumberRef类型。

  • kVTCompressionPropertyKey_DataRateLimits

    这个属性表示码率限制,单位是byte。该属性是一个CFNumberRefCFArrayRef类型。

//  是否实时编码输出
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_RealTime,
                     kCFBooleanTrue);
//  设置profile 和 level
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_ProfileLevel,
                     kVTProfileLevel_H264_Baseline_AutoLevel);
//  是否产生B帧
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_AllowFrameReordering,
                     kCFBooleanFalse);
//  设置关键帧间隔
int frameInterval = 10;
CFNumberRef frameIntervalRaf = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_MaxKeyFrameInterval,
                     frameIntervalRaf);
//  设置期望FPS
int fps = 10;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_ExpectedFrameRate,
                     fpsRef);
        
//  设置平均码率
int bitRate = self.width * self.height * 3 * 4 * 8;
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_AverageBitRate,
                     bitRateRef);
        
//  设置硬性码率限制
int bigRateLimit = self.width * self.height * 3 * 4;
CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);
VTSessionSetProperty(_encoderSession,
                     kVTCompressionPropertyKey_DataRateLimits,
                     bitRateLimitRef);

3. 准备编码

OSStatus status = VTCompressionSessionPrepareToEncodeFrames(encodeSesion);
if(status != noErr) {
    NSLog(@"prepare to encode error! [status : %d]", (int)status);
}

5. 编码

我们拿到CMSampleBuffer的数据就可以使用VTCompressionSessionEncodeFrame()来进行编码了。

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
self.frameID++;
CMTime timeStamp = CMTimeMake(self.frameID, 1000);
CMTime duration = kCMTimeInvalid;
VTEncodeInfoFlags flag;
OSStatus status = VTCompressionSessionEncodeFrame(self.encoderSession, pixelBuffer, timeStamp, duration, NULL, NULL, &flag);
if (status != noErr) {
    NSLog(@"encode sample buffer error [status : %d]", status);
}

6. 编码后处理

这部分在回调函数中执行。

(1). 判断编码状态
if (status != noErr) {
    return;
}
(2). 判断数据有没有准备好
Boolean isDataReady = CMSampleBufferDataIsReady(sampleBuffer);
if (!isDataReady) {
   return;
}
(3). 获得当前对象

这个步骤依赖于我们在创建会话的时候传入的参考值

KKKVideoCoder *coder = (__bridge KKKVideoCoder *)(outputCallbackRefCon);
(4). 判断关键帧(I帧)
CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
if (!attachmentsArray) {
   return;
}
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachmentsArray, 0);
if (!dict) {
   return;
}
Boolean isIFrame = false;
isIFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
  • 关于kCMSampleAttachmentKey_NotSync,官方文档有这样一段discussion:

A sync sample, also known as a key frame or IDR (Instantaneous Decoding Refresh), can be decoded without requiring any previous samples to have been decoded. Samples following a sync sample also do not require samples prior to the sync sample to have been decoded. Samples are assumed to be sync samples by default — set the value for this key to kCFBooleanTrue for samples which should not be treated as sync samples.
This attachment is read from and written to media files.

简单翻译如下:

一个同步样本,即关键帧或者IDR,可以在不解码任何之前的样本的情况下进行解码。一个同步样本之后的样本也不需要这个同步样本之前的样本完成解码。默认情况下,样本被假定为同步样本。— 若样本不被视为同步样本,则会将这个key设置为kCFBooleanTrue。
该附件是从媒体文件读取和写入的。

(5). 从关键帧(I帧)获取SPS和PPS

如果我们拿到的是关键帧(I帧),我们就需要在数据前拼接相应的SPS和PPS。

获取描述信息
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
获取SPS
size_t spsSize, spsCount;
const uint8_t *spsData;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0);
if (spsStatus == noErr) {
 
    coder.hasSPS = YES;
    NSMutableData *sps = [NSMutableData dataWithCapacity:4 + spsSize];
    [sps appendBytes:startCode length:4];
    [sps appendBytes:spsData length:spsSize];
     
    dispatch_async(coder.callBackQueue, ^{
        [coder.delegate encoderGetSPSData:sps];
    });
} else {
    NSLog(@"get SPS error! [status : %d]", spsStatus);
}
获取PPS
size_t ppsSize, ppsCount;
const uint8_t *ppsData;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0);
if (ppsStatus == noErr) {
      
    coder.hasPPS = YES;
    NSMutableData *pps = [NSMutableData dataWithCapacity:4 + ppsSize];
    [pps appendBytes:startCode length:4];  //  startCode是"\x00\x00\x00\x01"
    [pps appendBytes:ppsData length:ppsSize];
      
    dispatch_async(coder.callBackQueue, ^{
        [coder.delegate encoderGetPPSData:pps];
    });
} else {
    NSLog(@"get PPS error! [status : %d]", ppsStatus);
}
(6). 处理编码后的数据
获取编码后的数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    
size_t lengthAtOffsetOut, totalLengthOut;
char * dataPointOut;
OSStatus error = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffsetOut, &totalLengthOut, &dataPointOut);
if (error != kCMBlockBufferNoErr) {
    NSLog(@"get block buffer data pointer failed! [status : %d]", error);
}
循环从dataBuffer获取NALU流数据

注意:返回的NALU数据前4个字节不是起始位置,而是大端模式的帧长度length

size_t offset = 0;
const int startCodeLength = 4;
while (offset < totalLengthOut - startCodeLength) {
   //  得到当前处理的NAL单元的起始位置指针
   char *src = dataPointOut + offset;
   
   //  获取NAL单元长度(此时为大端长度)
   uint32_t naluBigLength = 0;
   memcpy(&naluBigLength, src, startCodeLength);
   
   //  将获取的长度转化为小端长度
   uint32_t naluHostLength = CFSwapInt32BigToHost(naluBigLength);
   
   //  得到NAL单元的整体长度
   uint32_t naluLength = startCodeLength + naluHostLength;
   
   //  拼接数据
   NSMutableData *data = [NSMutableData dataWithCapacity:naluLength];
   [data appendBytes:startCode length:4];
   [data appendBytes:src + startCodeLength length:naluHostLength];
   
   dispatch_async(coder.callBackQueue, ^{
       [coder.delegate encoderGetData:data];
   });
   
   offset += naluLength;
}

7. 编码结束

我们可以再dealloc方法中结束编码操作

- (void)dealloc {
    
    if (self.encoderSession) {
        
        VTCompressionSessionCompleteFrames(self.encoderSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(self.encoderSession);
        CFRelease(self.encoderSession);
        self.encoderSession = NULL;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容