引言

视频编解码是音视频技能中重要的一部分,苹果在WWDC2014开发者大会开放了支撑视频硬编解码功能的Video Toolbox结构,本文旨在介绍该结构的根底知识、运用细节和常见问题等内容。

Video Toolbox根底

软编解码和硬编解码

视频编码是为了紧缩视频数据,下降视频传输或存储开支,视频解码则是为了播映编码后的视频进行的逆向操作。视频的编解码分为软编解码和硬编解码:

编解码类型 编解码硬件 优点 缺陷
软编解码 CPU 兼容性好,升级便利,支撑一切视频格局,画质清晰。 功能较差的机型会发热或卡顿。
硬编解码 非CPU:GPU或专用的DSP、FPGA、ASIC芯片等 对CPU占用率低,不会呈现手机发热等现象。 某些设备硬件不支撑,兼容性不好。

基于以上比照,现在大部分事务场景的编解码战略是:手机端采用硬编码生成视频文件发送给服务器,服务器进行软编转码为支撑更多的格局或码率的视频,再分发给观看端。考虑到有些设备不支撑硬编解码,一般需求软编解码做兜底。

Annex-B 和 AVCC/HVCC

咱们常见的视频编码格局H.264/AVC是由国际规范组织机构(ISO)下属的运动图象专家组(MPEG)和国际电传视讯联盟远程通讯规范化组织(ITU-T)开发的系列编码规范,之后又相继推出了H.265/HEVCH.266/VVC,iOS现在尚不支撑H.266,本文以H.264和H.265格局介绍Video Toolbox的功能。

为了获得高的视频紧缩比和网络亲和性(即适用于各种传输网络),将H.264体系架构分为了视频编码层VCL(Video Coding Layer)和网络笼统层(或网络适配层)NAL(Network Abstraction Layer),后续H.265、H.266也沿用了这一分层架构。

  • 视频编码层(VCL):用于独立于网络进行视频编码,编码完结后输出SODB(String Of Data Bits)数据。
  • 网络笼统层(NAL):该层的作用是将视频编码数据依据内容的不同划分红不同类型的NALU,以适配到各式各样的网络和多元环境中。对VCL输出的SODB数据后增加完毕比特,一个比特 1 和若干个比特 0,用于字节对齐,称为RBAP,然后再在 RBSP 头部加上 NAL Header 来组成一个一个的NAL单元(unit)即NALU

H.264和H.265的NAL Header结构:

iOS Video Tool box 视频硬编解码

NAL Header包括当时NALU的类型(nal_unit_type)信息,H.264的NAL Header为一个字节,H.265为两个字节,所以获取类型的办法不同: H.264为:int type = (frame[4] & 0x1F),5代表I帧,7、8别离代表SPS、PPS。 H.265为:int type = int type = (code & 0x7E)>>1,19代表I帧,32、33、34别离代表VPS、SPS、PPS。

因为NALU长度不一,要写到一个文件中需求标识符来分割码流以区别独立的NALU,解决这一问题的两种计划,产生了两种不同的码流格局:

  • Annex-B:在每个NALU前加上0 0 0 1或许0 0 1,称作start code(开端码),假如原始码流中含有开端码,则起用防竞争字节:如将0 0 0 1处理为0 0 0 3 1。
  • AVCC/HVCC:在NALU前面加上几个字节,用于表明整个NALU的长度(大端序,读取时调用CFSwapInt32BigToHost()转为小端),在读取的时分先将长度读取出来,再读取整个NALU。

除了NALU前增加的字节表明的含义不同之外,AVCC/HVCC和Annex-B在处理序列参数集SPS(Sequence Parameter Set)、图画参数集PPS(Picture Parameter Set)和视频参数集VPS(Video Parameter Set)(H.265才有)上也不同(不是有必要要对参数集的具体内容做具体了解,咱们只要知道这些参数集是解码所必需的数据,在解码前需求拿到这些数据即可)。

H.264能够经过CMVideoFormatDescriptionGetH264ParameterSetAtIndex获取,0、1别离对应SPS和PPS。H.265能够经过CMVideoFormatDescriptionGetHEVCParameterSetAtIndex获取,0、1、2别离对应VPS、SPS和PPS。后面会有完整代码示例。

Annex-B和AVCC/HVCC对参数集的不同处理方法:

  • Annex-B:参数集当成普通的NALU处理,每个I帧前都需求增加(VPS/)SPS/PPS。
  • AVCC/HVCC:参数集特别处理,放在头部被称为extradata的数据中。

iOS Video Tool box 视频硬编解码

为什么不统一为一种格局?

咱们知道视频分为本地视频文件和网络直播流,关于直播流,AVCC/HVCC 格局只在头部增加了参数集,假如是半途进入观看会获取不到参数集,也就无法初始化解码器进行解码,而 Annex-B 在每个I帧前都增加了参数集,能够从最近的I帧初始化解码器解码观看。而 AVCC/HVCC 只在头部增加参数集很合适用于本地文件,解码本地文件只需求获取一次参数集进行解码就能播映,所以不需求像Annex-B相同重复地存储多份参数集。

为什么要了解这两种格局?

因为Video Toolbox编码和解码只支撑 AVCC/HVCC 的码流格局,而Android的 MediaCodec 只支撑 Annex-B 的码流格局。因此在流媒体场景下,关于iOS开发而言,需求在收集编码之后转为Annex-B格局再进行推流,拉流解码时则需求转为AVCC/HVCC格局才能用Video Toolbox进行解码播映。

假如在编码后想直接存储为本地文件,能够运用AVFoundation结构中的AVAssetWriter。

Video Toolbox结构概览

Video Toolbox 最早是在OS X上运转,现在看苹果的官方文档的阐明,Video Toolbox 的体系支撑为iOS 6.0+,实际上苹果在WWDC2014大会上才开放了Video Toolbox结构,即iOS 8.0今后开发者才能够运用。

iOS Video Tool box 视频硬编解码

官方文档介绍:Video Toolbox是一个底层结构,供给对硬件编码器和解码器的直接拜访。它供给了视频编码和解码服务,以及存储在CoreVideo像素缓冲区的光栅图画格局之间的转换。这些服务以session(编码、解码和像素转换)的方法供给,并以Core Foundation (CF)类型供给。不需求直接拜访硬件编码器和解码器的应用程序不应该直接运用VideoToolbox。

Video Toolbox结构现在分为了编解码和像素转换两个模块,iOS9.0之后支撑了多通道编解码,本文要运用的是Compression的前两个类:VTCompressionSession(编码)和VTDecompressionSession(解码)。

iOS Video Tool box 视频硬编解码

Video Toolbox的输入和输出

在运用Video Toolbox前咱们先了解Video Toolbox的输入和输出。iOS开发一般运用AVFoundation结构进行视频录制,AVFoundation结构流通的数据类型为CMSampleBuffer,运用AVCapture模块录制视频的回调办法为- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection,编码需求的输入即原始视频帧CVImageBuffer(或CVPixelBuffer)就包裹在CMSampleBuffer中,经过编码后输出的仍为CMSampleBuffer类型,其间的CMBlockBuffer为编码后数据。相反,解码以CMSampleBuffer类型的CMSampleBuffer作为输入,解码完输出CVImageBuffer(CVPixelBuffer),CMSampleBuffer能够经过AVAssetReader读取文件得到,或许从流媒体中读取NSData运用CMSampleBufferCreateReady手动创立。

iOS Video Tool box 视频硬编解码

编码

VTCompressionSession的文档介绍了运用VTCompressionSession进行视频硬编码的作业流程:

1. Create a compression session using VTCompressionSessionCreate.
   // 运用VTCompressionSessionCreate创立一个编码会话。
2. Optionally, configure the session with your desired Compression Properties by calling VTSessionSetProperty or VTSessionSetProperties.
   // 可选地,经过调用VTSessionSetProperty或VTSessionSetProperties来装备编码器特点。
3. Encode video frames using VTCompressionSessionEncodeFrame and receive the compressed video frames in the session’s VTCompressionOutputCallback.
   // 运用VTCompressionSessionEncodeFrame编码视频帧,并在会话的VTCompressionOutputCallback中接纳编码后的视频帧。
4. To force the completion of some or all pending frames, call VTCompressionSessionCompleteFrames.
   // 要强制完结部分或一切挂起的帧,调用VTCompressionSessionCompleteFrames。
5. When you finish with the compression session, call VTCompressionSessionInvalidate to invalidate it and CFRelease to free its memory.
   // 当您完结编码会话时,调用VTCompressionSessionInvalidate来使其无效,并调用CFRelease来开释它的内存。

结合实际的编码和后续处理,逐渐解析相关API:

1. 创立编码会话VTCompressionSessionRef

/* 参数解析
allocator: session的内存分配器,传NULL表明默许的分配器。
width,height: 指定编码器的像素的宽高,与捕捉到的视频分辨率保持一致,假如视频编码器不能支撑供给的宽度和高度,它可能会改动它们。
codecType: 编码类型,如H.264:kCMVideoCodecType_H264、H.265:kCMVideoCodecType_HEVC,最好先调用VTIsHardwareDecodeSupported(codecType) 判别是否支撑该编码类型。
encoderSpecification: 指定有必要运用特定的编码器,传NULL的话Video Toolbox会自己挑选一个编码器。
sourceImageBufferAttributes: 原始视频数据需求的特点,主要用于创立CVPixelBufferPool,假如不需求Video Toolbox创立,能够传NULL,但是运用自己创立的CVPixelBufferPool会增加需求拷贝图画数据的几率。
compressedDataAllocator: 编码数据的内存分配器,传NULL表明运用默许的分配器.
outputCallback: 接纳编码数据的回调,这个回调能够挑选运用同步或异步方法接纳。假如用同步则与VTCompressionSessionEncodeFrame函数线程保持一致,假如用异步会新建一条线程接纳。该参数也可传NULL不过当且仅当咱们运用VTCompressionSessionEncodeFrameWithOutputHandler函数作编码时。
outputCallbackRefCon: 能够传入用户自定义数据,主要用于回调函数与主类之间的交互。
compressionSessionOut: 传入要创立的session的内存地址,留意,session不能为NULL。
*/
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));

其间,视频解码输出的回调参数outputCallback类型为typedef void (*VTCompressionOutputCallback)(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer),”sampleBuffer”即为编码后的数据。

2. 装备编码器特点

/*
session:会话
propertyKey:特点名称
propertyValue:特点值,设置为NULL将康复默许值。
*/
  VT_EXPORT OSStatus
  VTSessionSetProperty(
  CM_NONNULL VTSessionRef       session,
  CM_NONNULL CFStringRef        propertyKey,
  CM_NULLABLE CFTypeRef         propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

WWDC2022之后即iOS16.0之后新加了3个key,加上之前支撑的53个,一共支撑56个不同的key值装备编码参数。下表列举了常用的一些key值,简略感受一下能够装备的内容:

key 含义
kVTCompressionPropertyKey_RealTime 是否为实时编码,离线编码设置为kCFBooleanFalse,实时编码设置为kCFBooleanTrue。
kVTCompressionPropertyKey_ProfileLevel 设置session的profile和level,一般设置为kVTProfileLevel_H264_Baseline_AutoLevel, kVTProfileLevel_H264_Main_AutoLevel, kVTProfileLevel_H264_High_AutoLevel, kVTProfileLevel_HEVC_Main_AutoLevel等。
kVTCompressionPropertyKey_AllowFrameReordering 是否支撑B帧。为了对B帧进行编码,视频编码器有必要对帧进行从头排序,即解码与显现的次序会不同。
kVTCompressionPropertyKey_AverageBitRate 平均码率
kVTCompressionPropertyKey_DataRateLimits 码率上限
kVTCompressionPropertyKey_ExpectedFrameRate 希望帧率
kVTCompressionPropertyKey_MaxKeyFrameInterval 最大关键帧间隔帧数,即GOP 帧数
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration 最大关键帧间隔时长
kVTEncodeFrameOptionKey_ForceKeyFrame 是否强制为I帧
kVTCompressionPropertyKey_NumberOfPendingFrames 设置缓冲队列的巨细,设置为1,则每一帧都实时输出
kVTCompressionPropertyKey_AllowTemporalCompression 默许为true,设置为false则只编码关键帧。

3. 准备编码

// session: 编码会话
VT_EXPORT OSStatus
VTCompressionSessionPrepareToEncodeFrames( CM_NONNULL VTCompressionSessionRef session ) API_AVAILABLE(macosx(10.9), ios(8.0), tvos(10.2));

4. 开端编码

/*
session: 编码会话
imageBuffer: 一个CVImageBuffer,包括一个要紧缩的视频帧,引用计数不能为0。
presentationTimeStamp: PTS,此帧的显现时刻戳,会增加到CMSampleBuffer中。传递给session的每个PTS有必要大于前一帧的。
duration: 此帧的显现持续时刻,会增加到CMSampleBuffer中。假如没有持续时刻信息,则传递kCMTimeInvalid。
frameProperties: 指定此帧编码的特点的键/值对。留意,一些会话特点也可能在帧之间改动,这样的变化会对随后编码的帧产生影响。
sourceFrameRefCon: 帧的参阅值,它将被传递给输出回调函数。
infoFlagsOut: 指向VTEncodeFlags指针,用于接纳有关编码操作的信息。假如编码正在进行,能够设置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));

5. 处理编码后的数据

在回调outputCallback中处理编码后的数据,即CMSamplaBuffer。要将编码后的CMSamplaBuffer写入mp4、MOV等容器文件,能够运用AVFoundation结构AVAssetWriter。

咱们着重解说流媒体场景下如何将AVCC/HVCC转为Annex-B格局:

  1. 从关键帧中获取extradata,获取参数集
// 判别是否是关键帧
bool isKeyFrame = !CFDictionaryContainsKey((CFDictionaryRef)CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), (const void *)kCMSampleAttachmentKey_NotSync);

CMSampleBufferGetSampleAttachmentsArray用于获取样本的附加信息数据,苹果文档对kCMSampleAttachmentKey_NotSync的解释为:一个同步样本,也被称为关键帧或IDR(瞬时解码改写),能够在不需求任何之前的样本被解码的状况下被解码。同步样本之后的样本也不需求在同步样本之前的样本被解码。所以样本的附加信息字典不包括该key即为I帧。

    // 获取编码类型
    CMVideoCodecType codecType = CMVideoFormatDescriptionGetCodecType(CMSampleBufferGetFormatDescription(sampleBuffer));
    CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
    if (codecType == kCMVideoCodecType_H264) {
        // H.264/AVC 取出formatDescription中index 0、1对应的SPS、PPS
        size_t sparameterSetSize, sparameterSetCount, pparameterSetSize, pparameterSetCount;
        const uint8_t *sparameterSet, *pparameterSet;
        OSStatus statusCode_sps = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
        if (statusCode_sps == noErr) { // SPS
            NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        }
        OSStatus statusCode_pps = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
        if (statusCode_pps == noErr) { // PPS
            NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        }
    } else if (codecType == kCMVideoCodecType_HEVC) {
        // H.265/HEVC 取出formatDescription中index 0、1、2对应的VPS、SPS、PPS
        size_t vparameterSetSize, vparameterSetCount, sparameterSetSize, sparameterSetCount, pparameterSetSize, pparameterSetCount;
        const uint8_t *vparameterSet, *sparameterSet, *pparameterSet;
        if (@available(iOS 11.0, *)) { // H.265/HEVC 要求iOS11以上
            OSStatus statusCode_vps = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vparameterSet, &vparameterSetSize, &vparameterSetCount, 0);
            if (statusCode_vps == noErr) { // VPS
                NSData *vps = [NSData dataWithBytes:vparameterSet length:vparameterSetSize];
            }
            OSStatus statusCode_sps = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
            if (statusCode_sps == noErr) { // SPS
                NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
            }
            OSStatus statusCode_pps = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
            if (statusCode_pps == noErr) { // PPS
                NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
            }
    }
  1. 将参数集别离拼装成单个NALU
    // 增加start code(后续都以H.264为例)
    NSMutableData *annexBData = [NSMutableData new];
    uint8_t startcode[] = {0x00, 0x00, 0x00, 0x01};
    [annexBData appendBytes:nalPartition length:4];
    [annexBData appendData:sps];
    [annexBData appendBytes:nalPartition length:4];
    [annexBData appendData:pps];
  1. 将AVCC/HVCC格局中表明长度的4字节替换为start code
    // 获取编码数据。这里的数据是 AVCC/HVCC 格局的。
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;        
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int NALULengthHeaderLength = 4;
        // 拷贝编码数据。
        while (bufferOffset < totalLength - NALULengthHeaderLength) {
            // 经过 length 字段获取当时这个 NALU 的长度。
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, NALULengthHeaderLength);
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            // 增加start code    
            [annexBData appendData:[NSData dataWithBytes:startcode length:4]];
            [annexBData appendData:[NSData dataWithBytes:(dataPointer + bufferOffset + NALULengthHeaderLength) length:NALUnitLength]];                
            bufferOffset += NALULengthHeaderLength + NALUnitLength;
        }
    }

之后就能够进行上传推流或许直接将H.264/H.265写入文件了。

6. 完毕编码

    // 强制完结部分或一切挂起的帧
    VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid);
    // 毁掉编码器
    VTCompressionSessionInvalidate(_compressionSession);
    // 开释内存
    CFRelease(_compressionSession);

按是否需求转化格局来分,iOS硬编码流程图如下:

iOS Video Tool box 视频硬编解码

解码

iOS视频硬解码是个从CMSampleBuffer中获取视频帧CVImageBuffer和用于播映一切必要的显现时刻戳presentationTimeStamp和显现持续时刻presentationDuration的进程,Video Toolbox用于解码的类是VTDecompressionSession,文档也介绍了运用VTDecompressionSession进行视频硬解码的作业流程:

1. Create a decompression session by calling VTDecompressionSessionCreate.
   // 调用VTDecompressionSessionCreate创立一个解码会话
2. Optionally, configure the session with your desired Decompression Properties by calling VTSessionSetProperty or VTSessionSetProperties.
   // 可选地,经过调用VTSessionSetProperty或VTSessionSetProperties来装备解码器特点。
3. Decode video frames using VTDecompressionSessionDecodeFrame.
   // 解码视频帧运用VTDecompressionSessionDecodeFrame,并在解码会话的VTDecompressionOutputCallbackRecord回调中处理解码后的视频帧、显现时刻戳、显现持续时刻等信息。
4. When you finish with the decompression session, call VTDecompressionSessionInvalidate to tear it down, and call CFRelease to free its memory.
   // 完结解码会话时,调用VTDecompressionSessionInvalidate来删去它,并调用CFRelease来开释它的内存。

结合实际的解码和后续处理,逐渐解析相关API:

1. 创立解码会话VTDecompressionSessionRef

/*参数解析
allocator: session的内存分配器,传NULL表明默许的分配器。
videoFormatDescription: 源视频帧的描述信息。
videoDecoderSpecification: 指定有必要运用的特定视频解码器。传NULL则Video Toolbox会主动挑选一个解码器。
destinationImageBufferAttributes: 目标图画的特点要求。传NULL则不作要求。
outputCallback: 解码的回调函数。只能在调用VTDecompressionSessionDecodeFrameWithOutputHandler解码帧时传递NULL。
decompressionSessionOut: 指向一个变量以接纳新的解码会话。
*/
VT_EXPORT OSStatus 
VTDecompressionSessionCreate(
	CM_NULLABLE CFAllocatorRef                              allocator,
	CM_NONNULL CMVideoFormatDescriptionRef					videoFormatDescription,
	CM_NULLABLE CFDictionaryRef								videoDecoderSpecification,
	CM_NULLABLE CFDictionaryRef                             destinationImageBufferAttributes,
	const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback,
	CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

其间destinationImageBufferAttributes能够设置图画显现的分辨率(kCVPixelBufferWidthKey/kCVPixelBufferHeightKey)、像素格局(kCVPixelBufferPixelFormatTypeKey)、是兼容OpenGL(kCVPixelBufferOpenGLCompatibilityKey)等等,例如:

    NSDictionary *destinationImageBufferAttributes = @{
        (id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
        (id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:_config.width],
        (id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:_config.height],
        (id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
      };

videoFormatDescription需求分状况处理: 假如是经过AVAssetReader或许其他方法能够直接获取到CMSampleBuffer,咱们能够直接调用CMSampleBufferGetFormatDescription(sampleBuffer)获取。 假如是从流媒体读取等无法直接获取CMSampleBuffer的状况,咱们需求从Annex-B格局转为AVCC/HVCC,而且手动创立videoFormatDescription和CMSampleBuffer。

格局转换进程与编码正好相反:将start code 转为4字节的NALU长度;判别数据类型,假如是参数集则存储用于初始化解码器,假如是帧数据则进行解码。

    // 从流媒体读取NSData
    NSData *h264Data = ...;
    uint8_t *nalu = (uint8_t *)h264Data.bytes;
    // 获取NALU类型,以H.264为例
    int type = (naluData[4] & 0x1F);
    // 将start code 转为4字节的NALU长度
    uint32_t naluSize = frameSize - 4;
    uint8_t *pNaluSize = (uint8_t *)(&naluSize);    
    naluData[0] = *(pNaluSize + 3);
    naluData[1] = *(pNaluSize + 2);
    naluData[2] = *(pNaluSize + 1);
    naluData[3] = *(pNaluSize);
    // 处理不同类型数据
    switch (type) {
        case 0x05:
         // I帧,去解码(解码前先保证解码器会话存在,不然就创立)
            break;
        case 0x06:
            // SEI信息,不处理,H.265也有一些不必处理的信息,详情能够去了解一下H.265的type表
            break;
        case 0x07:
            // sps
            _spsSize = naluSize;
            _sps = malloc(_spsSize);
            // 从下标4(也便是第五个元素)开端复制数据
            memcpy(_sps, &naluData[4], _spsSize);
            break;
        case 0x08:
            // pps
            _ppsSize = naluSize;
            _pps = malloc(_ppsSize);
            // 从下标4(也便是第五个元素)开端复制数据
            memcpy(_pps, &naluData[4], _ppsSize);
            break;
        default:
            // 其他帧(1-5),去解码(解码前先保证解码器会话存在,不然就创立)
            break;
    }

运用参数集创立CMVideoFormatDescriptionRef:

    CMVideoFormatDescriptionRef videoDesc;
    const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
    const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
    int naluHeaderLen = 4;  // 大端模式开端位长度
    OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &videoDesc);

2. 装备解码器特点

/*
session: 会话
propertyKey: 特点名称
propertyValue: 特点值,设置为NULL将康复默许值。
*/
VT_EXPORT OSStatus 
VTSessionSetProperty(
  CM_NONNULL VTSessionRef       session,
  CM_NONNULL CFStringRef        propertyKey,
  CM_NULLABLE CFTypeRef         propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

苹果官方文档列举了可装备的key现在有24个,不再赘述,可查阅Decompression Properties。

3. 开端解码

/*
session: 解码器会话
sampleBuffer: 包括一个或多个视频帧的CMSampleBuffer目标
decodeFlags: kVTDecodeFrame EnableAsynchronousDecompression位表明视频解码器是否能够异步解紧缩帧。kVTDecodeFrame EnableTemporalProcessing位指示解码器是否能够推迟对输出回调的调用,以便以时刻(显现)次序进行处理。假如这两个标志都被清除,解压完结,输出回调函数将在VTDecompressionSessionDecodeFrame回来之前被调用。假如设置了其间一个标志,VTDecompressionSessionDecodeFrame可能会在调用输出回调函数之前回来。
sourceFrameRefCon: 解码标识。假如sampleBuffer包括多个帧,输出回调函数将运用这个sourceFrameRefCon值屡次调用。
infoFlagsOut: 指向VTEncodeInfoFlags指针,用于接纳有关解码操作的信息。假如解码正在进行,能够设置kVTDecodeInfo_Asynchronous,假如帧被删去;能够设置kVTDecodeInfo_FrameDropped;假如不想接纳次信息能够传NUL。
*/
VTDecompressionSessionDecodeFrame(
	CM_NONNULL VTDecompressionSessionRef	session,
	CM_NONNULL CMSampleBufferRef			sampleBuffer,
	VTDecodeFrameFlags						decodeFlags, // bit 0 is enableAsynchronousDecompression
	void * CM_NULLABLE						sourceFrameRefCon,
	VTDecodeInfoFlags * CM_NULLABLE 		infoFlagsOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

其间的sampleBuffer假如需求从NSData中读取,咱们需求运用视频帧的数据创立CMBlockBuffer,再结合参数集信息创立CMSampleBuffer:

    CVPixelBufferRef outputPixelBuffer = NULL;
    CMBlockBufferRef blockBuffer = NULL;
    CMBlockBufferFlags flag0 = 0;
    // 创立blockBuffer
    OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);
    CMSampleBufferRef sampleBuffer = NULL;
    const size_t sampleSizeArray[] = {frameSize};
    // 创立sampleBuffer
    status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _videoDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);

4. 处理解码后的数据

在解码的回调videoDecoderCallBack中咱们能够获取图画和显现时刻戳、显现持续时刻,结合OpenGL、Core Imge进行显现,不再深入介绍。

5. 完毕解码

// 毁掉解码器
VTDecompressionSessionInvalidate(self.decodeSession);
// 开释内存
CFRelease(self.decodeSession);

iOS不同场景的硬解码流程图如下

iOS Video Tool box 视频硬编解码

留意事项

  1. 前后台切换会导致编解码出错,需求从头创立会话。
  2. 有些视频流虽然是AVCC格局,但NALU size的巨细是3个字节,需求转为4字节格局。
  3. 切换分辨率时需求拿到新的参数集,重启解码器。
  4. 编码后的视频帧之间存在参阅关系,为防止遗漏最后几帧,需求在解码完最后一帧数据后调用VTDecompressionSessionWaitForAsynchronousFrames,该接口会等待一切未输出的视频帧输出完毕后再回来。

参阅链接

Video Toolbox苹果官方文档
Overview of the High Efficiency Video Coding (HEVC) Standard
H.264规范文档