iOS Video Tool box video hard codec

introduction

Video encoding and decoding is an important part of audio and video technology. Apple has opened a framework that supports video hard encoding and decoding at the WWDC2014 developer conference Video Toolbox. This article aims to introduce the basic knowledge, usage details and common problems of the framework.

Video Toolbox Basics

Soft codec and hard codec

Video encoding is to compress video data and reduce video transmission or storage overhead, while video decoding is to perform the reverse operation for playing the encoded video. Video codec is divided into soft codec and hard codec:

Codec type Codec hardware advantage shortcoming
soft codec CPU Good compatibility, easy to upgrade, support all video formats, clear picture quality. Models with poor performance can get hot or freeze.
hard codec Non-CPU: GPU or dedicated DSP, FPGA, ASIC chip, etc. The CPU usage rate is low, and there will be no phone fever. Some devices are not supported by hardware and have poor compatibility.

Based on the above comparison, the current encoding and decoding strategies for most business scenarios are: the mobile terminal uses hard coding to generate video files and send them to the server, and the server performs soft encoding and transcoding to support more formats or bit rates. Video, and then distributed to the viewing terminal . Considering that some devices do not support hard codec, soft codec is usually needed as a bottom line.

Annex-B and AVCC/HVCC

Our common video coding formats are a series of coding standards developed by the Moving Picture Experts Group ( ) under the H.264/AVCInternational Standards Organization ( ) and the International Telex Union Telecommunication Standardization Organization ( ). Not yet supported , features described in this article in H.264 and H.265 formats .ISOMPEGITU-TH.265/HEVCH.266/VVCH.266Video Toolbox

In order to obtain high video compression ratio and network affinity (that is, suitable for various transmission networks), the H.264 system architecture is divided into a video coding layer VCL(Video Coding Layer) and a network abstraction layer (or network adaptation layer) NAL( Network Abstraction Layer), the subsequent H.265 and H.266 also follow this layered architecture.

  • 视频编码层(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结构:

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的数据中。

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

我们知道视频分为本地视频文件和网络直播流,对于直播流,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以后开发者才可以使用。

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

Video Toolbox框架目前分为了编解码和像素转换两个模块,iOS9.0之后支持了多通道编解码,本文要使用的是Compression的前两个类:VTCompressionSession(编码)和VTDecompressionSession(解码)。

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手动创建。

编码

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视频硬解码是个从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不同场景的硬解码流程图如下

注意事项

  1. 前后台切换会导致编解码出错,需要重新创建会话。
  2. 有些视频流虽然是AVCC格式,但NALU size的大小是3个字节,需要转为4字节格式。
  3. 切换分辨率时需要拿到新的参数集,重启解码器。
  4. 编码后的视频帧之间存在参考关系,为避免遗漏最后几帧,需要在解码完最后一帧数据后调用VTDecompressionSessionWaitForAsynchronousFrames,该接口会等待所有未输出的视频帧输出结束后再返回。

参考链接

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

Guess you like

Origin juejin.im/post/7116148824237670436