iOS 系统中,H.264 视频流可以硬件解码吗? 具体如何实现?

关注者
338
被浏览
87,394

19 个回答

这个问题都问了两年多了,没有很好的回答,我最近正好搞定了iOS的硬解码 H.264,借这个问题来分享下经验。

其实至少从iPhone4开始,苹果就是支持硬件解码了,但是硬解码API一直是私有API,不开放给开发者使用,只有越狱才能使用,正常的App如果想提交到AppStore是不允许使用私有API的。

从iOS8开始,可能是苹果想通了,开放了硬解码和硬编码API,就是名为 VideoToolbox.framework的API,需要用iOS 8以后才能使用,iOS 7.x上还不行。

这套硬解码API是几个纯C函数,在任何OC或者 C++代码里都可以使用。

首先要把 VideoToolbox.framework 添加到工程里,并且包含以下头文件。

#include <VideoToolbox/VideoToolbox.h>

解码主要需要以下三个函数

VTDecompressionSessionCreate 创建解码 session

VTDecompressionSessionDecodeFrame 解码一个frame

VTDecompressionSessionInvalidate 销毁解码 session


首先要创建 decode session,方法如下:

        OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                              decoderFormatDescription,
                                              NULL, attrs,
                                              &callBackRecord,
                                              &deocderSession);

其中 decoderFormatDescription 是 CMVideoFormatDescriptionRef 类型的视频格式描述,这个需要用H.264的 sps 和 pps数据来创建,调用以下函数创建 decoderFormatDescription

CMVideoFormatDescriptionCreateFromH264ParameterSets

需要注意的是,这里用的 sps和pps数据是不包含“00 00 00 01”的start code的。


attr是传递给decode session的属性词典

        CFDictionaryRef attrs = NULL;
        const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
//      kCVPixelFormatType_420YpCbCr8Planar is YUV420
//      kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12
        uint32_t v = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
        const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &v) };
        attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);

其中重要的属性就一个,kCVPixelBufferPixelFormatTypeKey,指定解码后的图像格式,必须指定成NV12,苹果的硬解码器只支持NV12。


callBackRecord 是用来指定回调函数的,解码器支持异步模式,解码后会调用这里的回调函数。

如果 decoderSession创建成功就可以开始解码了。

            VTDecodeFrameFlags flags = 0;
            //kVTDecodeFrame_EnableTemporalProcessing | kVTDecodeFrame_EnableAsynchronousDecompression;
            VTDecodeInfoFlags flagOut = 0;
            CVPixelBufferRef outputPixelBuffer = NULL;
            OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(deocderSession,
                                                                      sampleBuffer,
                                                                      flags,
                                                                      &outputPixelBuffer,
                                                                      &flagOut);

其中 flags 用0 表示使用同步解码,这样比较简单。

其中 sampleBuffer是输入的H.264视频数据,每次输入一个frame。

先用CMBlockBufferCreateWithMemoryBlock 从H.264数据创建一个CMBlockBufferRef实例。

然后用 CMSampleBufferCreateReady创建CMSampleBufferRef实例。

这里要注意的是,传入的H.264数据需要Mp4风格的,就是开始的四个字节是数据的长度而不是“00 00 00 01”的start code,四个字节的长度是big-endian的。

一般来说从 视频里读出的数据都是 “00 00 00 01”开头的,这里需要自己转换下。

解码成功之后,outputPixelBuffer里就是一帧 NV12格式的YUV图像了。

如果想获取YUV的数据可以通过

    CVPixelBufferLockBaseAddress(outputPixelBuffer, 0);
    void *baseAddress = CVPixelBufferGetBaseAddress(outputPixelBuffer);

获得图像数据的指针,需要说明baseAddress并不是指向YUV数据,而是指向一个CVPlanarPixelBufferInfo_YCbCrBiPlanar结构体,结构体里记录了两个plane的offset和pitch。

但是如果想把视频播放出来是不需要去读取YUV数据的,因为CVPixelBufferRef是可以直接转换成OpenGL的Texture或者UIImage的。

调用CVOpenGLESTextureCacheCreateTextureFromImage,可以直接创建OpenGL Texture


从 CVPixelBufferRef 创建 UIImage

    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    UIImage *uiImage = [UIImage imageWithCIImage:ciImage];


解码完成后销毁 decoder session

VTDecompressionSessionInvalidate(deocderSession)


硬解码的基本流程就是这样了,如果需要成功解码播放视频还需要一些H.264视频格式,YUV图像格式,OpenGL等基础知识。


还是有很多小细节要处理的,无法在这里一一说明了,有人有问题可以在评论里讨论。

从解码到播放,大约1000行代码左右,主要是OpenGL渲染的代码比较多。


苹果官方的示例代码:

WWDC - Apple Developer


苹果的例子下载链接实效了,我也找不到那个例子,我自己写了一个。

stevenyao/iOSHardwareDecoder · GitHub

来回答一下这个问题:

iOS系统支持H.264的硬件解码.

从iOS 8.0开始, apple 公开了 VideoToolbox Framework API, 开发者可以通过VideoToolbox直接访问

iPhone/iPad设备硬件加速的编码和解码能力, VideoToolbox支持多种视频格式的编码和解码, 包括H.263, MPEG4,

H264等, 从iOS 11.0开始, VideoToolbox 增加了HEVC(H.265)的支持, 彰显了apple的强悍的技术能力.

Apple VideoToolbox对h264解码的支持非常完备, 可以支持H264的多种Profile, 最大可以支持到Baseline 5.2,

Main 5.2, Extended 5.0, High 5.2. 足以满足应用对h264解码的需求.


VideoToolbox framework 所使用的基本数据结构(CVPixelBuffer等)定义于CoreVideo, 是apple的Core

Foundation的组成部分. CoreVideo和VideoToolbox所提供的接口是标准C接口, 开发者可以用C/C++

/ObjectiveC/Swift来调用, 其接口设计清晰明快, 使用非常简单. 下文将详细阐述如何使用VideoToolbox来实现

H264解码.


1. 创建 VTDecompressionSessionRef 对象

首先需要创建 decompression session, 通过调用 VTDecompressionSessionCreate 来创建.

VTDecompressionSessionCreate 的定义如下:

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));


其中, allocator 是decompression session的分配器, 如果没有特殊要求, 传入 NULL即可, session会使用

默认的分配器.

videoFormatDescription 是用来描述视频格式信息的对象, 如果是H.264解码的话, 跟Android的MediaCodec一样

需要先得到h264的sps和pps信息, 再根据sps和pps来创建对应的视频格式描述对象. 伪代码如下:

uint8_t * parameterSetPointers[2] = { sps, pps };

int32_t parameterSetSizes [2] = { sps_size, pps_size };

CMVideoFormatDescriptionRef description = NULL;

size_t size[] = { parameterSetSizes[0], parameterSetSizes[1] };

CMVideoFormatDescriptionCreateFromH264ParameterSets(NULL, 2, (const uint8_t * const *)parameterSetPointers, size, 4, &description);


videoDecoderSpecification: 用来指定所需要创建的解码器, 通常可以传入NULL, 让系统根据视频格式描述来

自动选择创建合适的解码器.

destinationImageBufferAttributes: 用来指定解码输出的像素格式,视频宽度, 高度等信息, 通常为了性能最优起见,

像素格式应选择YUV420的格式, 如: kCVPixelFormatType_420YpCbCr8Planar 等

outputCallback: 解码输出的回调, 它是一个结构体, 定义如下:

struct VTDecompressionOutputCallbackRecord {

CM_NULLABLE VTDecompressionOutputCallback decompressionOutputCallback;

void * CM_NULLABLE decompressionOutputRefCon;

};

decompressionOutputCallback 就是回调函数, decompression session解码输出是通过这个回调异步输出的.

这边需要强调的是,当解码器不是工作在同步模式时, 这里的异步输出线程是从系统的线程池中获取空闲线程作为回调线程的,

所以开发者不能假定这个回调来自同一线程.

decompressionOutputRefCon 是开发者自定义的对象, decompression session会把这个指针作为回调函数的第一个

参数回传给开发者.

2. 使用 VTDecompressionSessionRef 进行解码

解码的函数定义如下:

VT_EXPORT OSStatus

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));

session 就是decompression session, 无需多言

sampleBuffer 是输入解码的数据, 这里需要注意的是, 虽然H264 NAL是标准所定义的H264解码器能处理的最小

解码单元, 但是apple的这个硬件解码器是不行的, ios的硬件解码器只能输入完整的一帧数据, 这就意味着, 当

要解码的H264是以 multi-slice方式编码的话, 我需要将一帧的多个slice 组合成一个frame再提交给解码器,

而且组合的frame中slice的边界不是H.264标准Annex B所定义的 nal start code 这种, 这里采用了AVCC 格式.

decodeFlag: 解码器的一些可设置的工作模式.

sourceFrameRefCon: 开发者定义的参考帧, 可以传入 NULL.

infoFlagsOut: 解码的状态, 可以根据需要决定是否要监视这些状态.

伪代码如下:

status = VTDecompressionSessionDecodeFrame(session, sampleBuffer, kVTDecodeFrame_EnableAsynchronousDecompression, NULL, &decodeInfoFlags);

3. 销毁 VTDecompressionSessionRef 对象

销毁 decompression session 非常简单, 调用VTDecompressionSessionInvalidate 和 CFRelease 就可以, 伪代码如下:

if( NULL != session ){

VTDecompressionSessionInvalidate(session);

CFRelease(session);

session = NULL;

}

需要注意的是, 因为解码是一个异步过程, 而且通常解码会延迟输出,销毁的时候 开发者有可能希望将输入的h264流全部解码输出, 所以在销毁前需要调用 VTDecompressionSessionFinishDelayedFrames 将缓存的Frame输出.

Reference:

1. apple 官方文档:

VideoToolbox | Apple Developer Documentation

2. 参考实现: webrtc 中 iOS h264 硬件编码解码的实现.

WebRTC Native Code

推荐相关阅读:

iOS系统H264视频硬件编解码说明 - 云信博客

关注“网易云信”公众号,回复“知乎”,获取2019技术干货合集>>>

网易云信,你身边的即时通讯和音视频技术专家,了解我们,请戳网易云信官网

想要阅读更多行业洞察和技术干货,请关注网易云信博客

更多精彩内容,关注网易云信知乎机构号哦。