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