iOS H264(AVC)硬件编码

    iOS H264(AVC)硬件编码苹果从iPhone4就开始支持硬件解码了,但是那时候硬解码API一直是私有API,不给开发者使用,只有越狱版的才可以使用,但是APP如果想提交到APPStore是不允许使用私有API的。

    直到iOS8发布,才开发了硬解码和硬编码API,就是名为 VideoToolbox.framework的API。这套硬解码就几个纯C函数,在任何OC或者C++代码里面都可以使用到。示范中我使用C++解码


主要需要以下三个函数:

VTDecompressionSessionCreate 创建解码 

sessionVTDecompressionSessionDecodeFrame 解码

VTDecompressionSessionInvalidate 销毁解码



 如果发现H264解码有问题,可以从这方面着手:

1)对应的sps,pps是否正确,即sps和pps发生变化时,有没有及时销毁解码器重新创建。(sps,pps是用来创建播放器的,不需要解码)。

2)送入解码器的数据,有没有把头换成四字节的大端模式的大小。

3)送入的数据,需要分片的有没有做分片处理。

4)送入解码器的NAL,顺序是不是对的, H.264/AVC标准对送到解码器的NAL单元顺序是有严格要求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则解码器不能够正确解码。



步骤:

1)导入框架把 VideoToolbox.framework 添加到工程里,并且包含以下头文件。 

#include "VideoToolbox/VideoToolbox.h"

2)创建解码器

OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,

m_decoderFormatDescription,

NULL, attrs,

&callBackRecord,

&m_decoderSession);


decoderFormatDescription:是CMVideoFormatDescriptionRef类型的视频格式描述,这个需要用H.264的sps和pps数据来创建,调用CMVideoFormatDescriptionCreateFromH264ParameterSets创建。

需要注意的是:

a)这里的sps和pps数据是不包含“00 00 00 01”的H.264的头;

b)一旦 sps和pps发生改变,需要销毁解码器,重新创建解码器,才能解码成功。(一般分辨率发生变化,sps和pps会改变),这是解码器的一个难点,每次的sps和pps都需要和之前的对比,一旦发现有改变先销毁之前的解码器,再重新创建再解码。

attr:是传递给解码器的属性词典,其中最重要的是,kCVPixelBufferPixelFormatTypeKey,指定解码后的图像格式,必须指定是YUV420sp的NV12。搜索一下420的有以下四种:

kCVPixelFormatType_420YpCbCr8Planar

kCVPixelFormatType_420YpCbCr8PlanarFullRange

kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange

kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

根据表面意思,可以看出,可以分为两类:planar(平面420p)和 BiPlanar(双平面)。我们需要的是NV12(双平面的),所以就选 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 或kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

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


具体代码:

static void acDidDecompress(void *decompressionOutputRefCon,

void *sourceFrameRefCon,

OSStatus status,

VTDecodeInfoFlags infoFlags,

CVImageBufferRef pixelBuffer,

CMTime presentationTimeStamp,

CMTime presentationDuration ){

CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;

*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);

}

bool iOSAVCDecoder::createAVCDecoder(const unsigned char *pps, const unsigned int pps_size, const unsigned char *sps, const unsigned int sps_size){

/*

*创建m_decoderFormatDescription 视频格式表述

*需要注意的是,这里用的 sps和pps数据是不包含“00 00 00 01”的H.264的。

*pps_size和sps_size就是去掉H.264头后的对应sps和pps的大小

*/

const unsigned char *const parameterSetPointers[2] = {pps, sps};

const size_t paramerterSetSizes[2] = {pps_size, sps_size};

OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,

2,

parameterSetPointers,

paramerterSetSizes,

4,

&m_decoderFormatDescription);

if (noErr == status) {

/*

*解码器属性attrs

*/

CFDictionaryRef attrs = NULL;

const void *keys[] = {kCVPixelBufferPixelFormatTypeKey};

unsigned int v = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;//NV12

const void *values[] = {CFNumberCreate(NULL, kCFNumberSInt32Type, &v)};

attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);

/*

*解码器回调callBackRecord

*/

VTDecompressionOutputCallbackRecord callBackRecord;

callBackRecord.decompressionOutputCallback = acDidDecompress;

callBackRecord.decompressionOutputRefCon = NULL;

/*

*最后传入参数,创建解码器

*/

status = VTDecompressionSessionCreate(kCFAllocatorDefault, m_decoderFormatDescription, NULL, attrs, &callBackRecord, &m_decoderSession);

CFRelease(attrs);

} else {

return false;

}

return true;

}

3)解码一帧数据

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

VTDecodeFrameFlags flags = 0;  //kVTDecodeFrame_EnableTemporalProcessing | kVTDecodeFrame_EnableAsynchronousDecompression;

VTDecodeInfoFlags flagOut = 0;

CVPixelBufferRef outputPixelBuffer = NULL;

OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(m_decoderSession,

sampleBuffer,

flags,

&outputPixelBuffer,

&flagOut);


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

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

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

然后用 CMSampleBufferCreateReady创建CMSampleBufferRef实例。

这里要注意的是:

a)传入的H.264数据需要Mp4风格的,就是开始的四个字节是数据的长度而不是“00 00 00 01”的start code,四个字节的长度是big-endian(大端模式)。 一般来说从 视频里读出的数据都是 “00 00 00 01”H.264的头的,所以一般拿到数据都需要转换(H.264的头转成4字节大小且是大端模式的四字节大小)。

b) 另外传入的一帧数据,可能不只只有前面有“00 00 00 01”,中间也有这样的H.264的头,所以传入到解码器解码之前,需要一一查找,一旦遇到这种一帧里面有N多个“00 00 01”的数据,就要进行分片处理(即把所有的“00 00 00 01”换成big-endian模式的4字节大小,这个大小是当前头到下一个头中间的数据大小)。

3)除了“00 00 00 01”还有“00 00 01”(一般这种头都是在P帧中)这种头,也需要替换。(即把所有的“00 00 01”换成big-endian模式的4字节大小,这个大小是当前头到下一个头中间的数据大小),同时注意,有三字节的头换成4字节的大小,这时候帧的总大小发生了变化。

这个大小替换和分片处理,此处就不给出示例代码了。

outputPixelBuffer:输出的YUV数据

具体代码:

CVPixelBufferRef iOSAVCDecoder::DecodeFrame(const ACStreamPacket *packet,ACVideoFrame *frame){

CVPixelBufferRef outputPixelBuffer = NULL;

/*

*CMBlockBufferCreateWithMemoryBlock创建CMBlockBufferRef实例

*/

CMBlockBufferRef blockBuffer = NULL;

OSStatus status  = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,

(void*)packet->buffer, packet->size,

kCFAllocatorNull,

NULL, 0, packet->size,

0, &blockBuffer);

if(status == kCMBlockBufferNoErr) {

/*

*CMSampleBufferCreateReady创建CMSampleBufferRef实例

*/

CMSampleBufferRef sampleBuffer = NULL;

const size_t sampleSizeArray[] = {packet->size};

status = CMSampleBufferCreateReady(kCFAllocatorDefault,

blockBuffer,

m_decoderFormatDescription ,

1, 0, NULL, 1, sampleSizeArray,

&sampleBuffer);

/*

*开始解码

*/

if (status == kCMBlockBufferNoErr && sampleBuffer) {

VTDecodeFrameFlags flags = 0;//kVTDecodeFrame_EnableTemporalProcessing |kVTDecodeFrame_EnableAsynchronousDecompression; 0表示同步解码

VTDecodeInfoFlags flagOut = 0;

OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(m_decoderSession,

sampleBuffer,

flags,

&outputPixelBuffer,

&flagOut);

if(decodeStatus != noErr) {

printf("h264: decode failed status=%d\n", decodeStatus);

}

else{

/*********************** 下面是读YUV 数据  ************************/

size_t width = (uint32_t)CVPixelBufferGetWidth(outputPixelBuffer);//视频真实分辨率宽

size_t height = (uint32_t)CVPixelBufferGetHeight(outputPixelBuffer);//视频真实分辨率高

/*

*备注一下:如果我创建解码器时,选择的pixelformate 是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,y和uv的RowOfPlane就是64字节补齐,比如width= 480,就补成512(补成64的倍数)

*但我选择kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,就不会补齐,,比如width= 480,y和uv的RowOfPlane还是480

*不是很理解这两种格式了??我这里选择的是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

*/

size_t yPlaneBytesPerRow = (uint32_t)CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 0);//Y-w 64字节对齐 非对齐的补0

size_t yPlaneHeight = (uint32_t)CVPixelBufferGetHeightOfPlane(outputPixelBuffer, 0);//Y-h

size_t uvPlaneBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 1);//uv-w = Y-w 64字节对齐 非对齐的补0

size_t uvPlaneHeight = CVPixelBufferGetHeightOfPlane(outputPixelBuffer, 1);//uv-h = Y-h/2

size_t frameSize = frame->stride*frame->slice + uvPlaneBytesPerRow*uvPlaneHeight;

frame->opaque = CVPixelBufferRetain(outputPixelBuffer);//自定义

frame->timestamp = packet->timestamp;

frame->offset = 0;

frame->width = width;

frame->height = height;

frame->stride = yPlaneBytesPerRow;

frame->slice = yPlaneHeight;

frame->size = (uint32_t)frameSize;

if (CVPixelBufferLockBaseAddress(outputPixelBuffer, 0) == kCVReturnSuccess) {

if (m_videoFrameBufferLength < frameSize) {//开辟m_videoFrameBuffer空间,不够就要realloc

unsigned char *p = (unsigned char *)realloc(m_videoFrameBuffer, frameSize + 1);

m_videoFrameBufferLength = (uint32_t)frameSize + 1;

m_videoFrameBuffer = p;

}

frame->capacity = m_videoFrameBufferLength;

frame->buffer = m_videoFrameBuffer;

memset(frame->buffer, 0, m_videoFrameBufferLength);

size_t buffSize = 0;

if (CVPixelBufferIsPlanar(outputPixelBuffer)) {//是NV12

if (frame->buffer) {

void *yPlaneAddress = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 0);//y

if (yPlaneAddress) {

memcpy(frame->buffer, yPlaneAddress, frame->stride * frame->slice);//拷贝Y数据

buffSize += frame->stride * frame->slice;

}

void *uvPlaneAddress = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 1);//uv

if (uvPlaneAddress) {

memcpy(frame->buffer + buffSize, uvPlaneAddress, uvPlaneBytesPerRow*uvPlaneHeight);//拷贝uv数据

buffSize += uvPlaneBytesPerRow*uvPlaneHeight;

}

}

} else {

return nil;

}

CVPixelBufferUnlockBaseAddress(outputPixelBuffer, 0);

CVPixelBufferRelease(outputPixelBuffer);

/*********************** 读YUV 数据完毕 ************************/

}

CFRelease(sampleBuffer);

}

CFRelease(blockBuffer);

}

}

return outputPixelBuffer;

}

4)读解码后的YUV数据

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

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

调用CVOpenGLESTextureCacheCreateTextureFromImage,可以直接创建OpenGL Texture。(这播放视频还是需要一些OpenGL的知识,这里先不想细介绍了)

如果想从CVPixelBufferRef创建UIImage:

CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];

UIImage *uiImage = [UIImage imageWithCIImage:ciImage];

但是有时候还是需要因为合作的关系,需要拿到YUV的数据,就使用上面解码中“读YUV 数据 ”来获取。

5)销毁解码器

没有什么特殊的,直接上代码:

bool iOSAVCDecoder::destoryDecoder(){

if(m_decoderSession) {

VTDecompressionSessionInvalidate(m_decoderSession);//销毁

m_decoderSession = NULL;

CFRelease(m_decoderFormatDescription);

m_decoderFormatDescription = NULL;

}

return true;

}

你可能感兴趣的:(iOS H264(AVC)硬件编码)