iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 本文档基于H.264的解码,介绍读写Video Toolbox解码回调函数参数CVImageBufferRef中的YUV或RGB数据的方法,并给出CVImageBufferRef生成灰度图代码、方便调试。同时,还介绍了Video Toolbox解码回调中进行YUV处理时容易忽略的问题。文档定位于iOS音视频高级编程,致力于提供高参考价值的Core Video中文资料,最近也在StackOverflow上关注Core Video相关问题,学习并回馈社区。 目录 |- 读取CVImageBufferRef(CVPixelBufferRef) |- 写入CVImageBufferRef(CVPixelBufferRef) |- CVPixelBufferPool内存池 |- CVPixelBuffer经过Core Graphics建立灰度图 |- 坑 |-- 直接操做解码回调的CVImageBuffer(CVPixelBuffer)存在的问题 |-- CVPixelBuffer上传至GPU后图像垂直镜像问题 |- 参考与推荐阅读 在实现全景视频播放器及其关联项目过程当中,我编写了如下Video Toolbox相关文档(因开发任务等缘由,部分文档处于草稿状态,以后会进行内容修订): iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):1 概述 【草稿】iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件 iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):4 同步编码 iOS 音视频高级编程:AVAssetReaderTrackOutput改变CMFormatDescription致使Video Toolbox解码失败与不解码GPU直接显示H.264帧 iOS 音视频高级编程:AVAsset、CoreVideo、VideoToolbox、FFmpeg与CMTime Video Toolbox Multi-pass Encoding 获取VideoToolbox解码直播等H.264流的颜色转换矩阵 CVPixelBufferRef是CVImageBufferRef的别名,二者操做几乎一致。 // CVPixelBuffer.h/* * CVPixelBufferRef * Based on the image buffer type. * The pixel buffer implements the memory storage for an image buffer. */typedef CVImageBufferRef CVPixelBufferRef; 虽然语法上CVPixelBufferRef是CVImageBufferRef的别名,它们在文档中的说明却有区别: Core Video image buffers provides a convenient interface for managing different types of image data. Pixel buffers and Core Video OpenGL buffers derive from the Core Video image buffer. CVImageBufferRef:A reference to a Core Video image buffer. An image buffer is an abstract type representing Core Video buffers that hold images. In Core Video, pixel buffers, OpenGL buffers, and OpenGL textures all derive from the image buffer type. CVPixelBufferRef :A reference to a Core Video pixel buffer object. The pixel buffer stores an image in main memory. 从上述可知,CVPixelBuffer『继承了』CVImageBuffer,然而,因为Core Video暴露出来的是Objective-C接口,意味着若想用C语言实现『面向对象的继承』,则CVPixelBuffer的数据成员定义位置与CVImageBuffer基本保持一致且令编译器进行相同的偏移以确保字节对齐,犹如FFmpeg中AVFrame可强制转换成AVPicture,以FFmpeg 3.0源码为例。 typedef struct AVFrame { uint8_t *data[AV_NUM_DATA_POINTERS]; int linesize[AV_NUM_DATA_POINTERS]; uint8_t **extended_data; // 后续还有众多字段}typedef struct AVPicture { ///< pointers to the image data planes uint8_t *data[AV_NUM_DATA_POINTERS]; ///< number of bytes per line int linesize[AV_NUM_DATA_POINTERS]; } AVPicture; 固然,从苹果开源的某些框架上看,Core Video内部极有可能用Objective-C++实现,可能真正用了C++式继承,在此不做过多猜想。 一、读取CVImageBufferRef(CVPixelBufferRef) 在解码回调中,传递过来的帧数据由CVImageBufferRef指向。若是需取出其中像素数据做进一步处理,得访问其中真正存储像素的内存。 VideoToolbox解码后的图像数据并不能直接给CPU访问,需先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,不然调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。值得注意的是,CVPixelBufferLockBaseAddress自身的调用并不消耗多少性能,通常状况,锁定以后,往CVPixelBuffer拷贝内存才是相对耗时的操做,好比计算内存偏移。若是CVPixelBuffer的图像须要显示在屏幕上,建议用GPU实现图像处理操做。下面展现读写左半图像时的性能损耗(请忽略内存计算的粗暴代码)。 读取CVPixelBuffer图像的性能消耗 写入CVPixelBuffer图像的性能消耗 然而,用CVImageBuffer -> CIImage -> UIImage则无需显式调用锁定基地址函数。 // CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); // 能够不加CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];CIContext *temporaryContext = [CIContext contextWithOptions:nil];CGImageRef videoImage = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(imageBuffer), CVPixelBufferGetHeight(imageBuffer))];UIImage *image = [[UIImage alloc] initWithCGImage:videoImage];UIImageView *imageView = [[UIImageView alloc] initWithImage:image];CGImageRelease(videoImage);// CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferIsPlanar可获得像素的存储方式是Planar或Chunky。如果Planar,则经过CVPixelBufferGetPlaneCount获取YUV Plane数量。一般是两个Plane,Y为一个Plane,UV由VTDecompressionSessionCreate建立解码会话时经过destinationImageBufferAttributes指定须要的像素格式(可不一样于视频源像素格式)决定是否同属一个Plane,每一个Plane可看成表格按行列处理,像素是行顺序填充的。下面以Planar Buffer存储方式做说明。 CVPixelBufferGetPlaneCount获得像素缓冲区平面数量,而后由CVPixelBufferGetBaseAddressOfPlane(索引)获得相应的通道,通常是Y、U、V通道存储地址,UV是否分开由解码会话指定,如前面所述。而CVPixelBufferGetBaseAddress返回的对于Planar Buffer则是指向PlanarComponentInfo结构体的指针,相关定义以下: /* Planar pixel buffers have the following descriptor at their base address. Clients should generally use CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane, etc. instead of accessing it directly. */struct CVPlanarComponentInfo { /* offset from main base address to base address of this plane, big-endian */ int32_t offset; /* bytes per row of this plane, big-endian */ uint32_t rowBytes; };typedef struct CVPlanarComponentInfo CVPlanarComponentInfo;struct CVPlanarPixelBufferInfo { CVPlanarComponentInfo componentInfo[1]; };typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;struct CVPlanarPixelBufferInfo_YCbCrPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCb; CVPlanarComponentInfo componentInfoCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;struct CVPlanarPixelBufferInfo_YCbCrBiPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCbCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrBiPlanar CVPlanarPixelBufferInfo_YCbCrBiPlanar; 根据CVPixelBufferGetPixelFormatType获得像素格式,以对应的方式读取,好比YUV420SP跨距读取全部的U到一个缓冲区。 二、写入CVImageBufferRef(CVPixelBufferRef) 下面代码展现了以向Y、UV Planar拷贝数据的过程: NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; CVPixelBufferRef pixelBuffer = NULL; CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, (__bridge CFDictionaryRef)pixelAttributes) &pixelBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, 0);uint8_t *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);memcpy(yDestPlane, yPlane, width * height);uint8_t *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);// numberOfElementsForChroma为UV宽高乘积memcpy(uvDestPlane, uvPlane, numberOfElementsForChroma); CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);if (result != kCVReturnSuccess) { NSLog(@"Unable to create cvpixelbuffer %d", result); } CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; CVPixelBufferRelease(pixelBuffer); 上述代码经过- [CIImage imageWithCVPixelBuffer:]建立CIImage在iPad Air 二、iPhone 6p等真机上存在的问题: 一、当使用kCVPixelFormatType_420YpCbCr8PlanarFullRange时提示[CIImage initWithCVPixelBuffer:options:] failed because its pixel format f420 is not supported.,即不支持由YUV420P格式的CVPixelBuffer建立CIImage。 经测试,视频源格式为yuvj420p(pc, bt709),在VTDecompressionSessionCreate不指定destinationImageBufferAttributes的kCVPixelBufferPixelFormatTypeKey值时,Video Toolbox解码出来的CVImageBufferRef对应为f420。 当指定destinationImageBufferAttributes须要kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange时,解码出来的ImageBuffer为420v,而后建立YUV时指定PixelFormat为f420会出现上述问题。缘由是,以420v方式拷贝YUV数据,其存储布局与f420不一样,致使建立CIImage失败。 二、决定CVPixelBufferCreate建立的格式是其参数pixelFormatType,而非参数pixelAttributes使用kCVPixelBufferPixelFormatTypeKey指定的像素格式。 下面介绍一些简单的图像处理办法。 原始灰度图 (一)水平镜像 水平镜像就是图像绕图像中间垂直线交换左右像素点位置,使用矩阵运行表示为: [x, y, 1] -1 0 0 -> [x', y', 1] 0 1 0 width 0 1 对于CPU而言,矩阵运行一般没GPU快,由于GPU作2x二、3x3等矩阵运算是硬件加速实现的,极可能就是一条指令处理完,而CPU每每是逐个元素进行计算,所以,目前你们倾向于GPU作矩阵运行。示例CPU实现代码以下。 for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[line * 960 + col] = src_buffer[line * 960 + (960 - col)]; } } iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 水平镜像 (二)垂直镜像 垂直镜像就是图像绕图像中间水平线交换上下像素点位置,使用矩阵运行表示为: [x, y, 1] 1 0 0 -> [x', y', 1] 0 -1 0 0 height 1 示例CPU实现代码以下。 for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[(480 - line) * 960 + col] = src_buffer[line * 960 + col]; } } iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 垂直镜像 三、CVPixelBufferPool内存池 自行建立CVPixelBufferPool且经过CVPixelBufferPool建立CVPixelBuffer,容易出现CVPixelBuffer被错误释放或意外增长引用计数致使内存泄露,以ijkplayer为例演示CVPixelBubffer泄露的状况。 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 CVPixelBuffer泄露 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 CVPixelBuffer结束引用时引用计数不为0致使内存泄露 而自行建立CVPixelBuffer,则容易出现内存暴涨问题,如建立一个960x480的YUV420SP格式的CVPixelBuffer所占内存为700多M,若是是异步解码且没做内存大小限制,将致使应用崩溃。 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 CVPixelBufferCreate占用的内存 若是不想自行建立CVPixelBufferPool,也不想本身建立CVPixelBuffer,取巧的办法是,使用解码回调函数的CVPixelBuffer,则无需担忧内存消耗问题。在实践过程当中,图像处理后当即编码,这样使用的场合不会致使解码器自身的缓存队列数据出现图像紊乱。前提是,修改后的像素数据在原数据的宽高范围内。固然,这也会出现些问题,具体在文档后续部分进行讨论 对于解码->图像处理->编码流程,且处理后的图像与原图像大小不一样,则建立编码器时再建立CVPixelBufferPool,让系统管理CVPixelBuffer也是可靠的作法。 另外,在图像处理过程当中,Video Toolbox不管指定FullRange仍是VideoRange,由此经过Core Graphics建立RGB图像是正确的,和QuickTime播放时画面保持一致。然而,解码出来的YUV420SP数据通过拷贝,接着进行图像处理,存在部分区域颜色有误。经过指定Video Toolbox输出YUV420P,再进行图像处理则无颜色异常问题。固然,使用的算法也改变相应的YUV420P算法,由于我的认为,这极有多是咱们团队的YUV420SP拷贝及操做算法有误。 四、CVPixelBuffer经过Core Graphics建立灰度图 修改完YUV数据后,若是每次都须要GPU实现YUV转换RGB,这比较麻烦,特别是转码等离线计算场合。下面,介绍一种实现CVPixelBuffer生成UIImage的办法,只使用Y平面生成图像,判断图像成像方面的处理结果是否符合预期。 // baseAddress为Y平面地址,传递yuv420(s)p完整数据地址,则忽略uvUIImage* yuv420ToUIImage(void *baseAddress, size_t width, size_t height, size_t bytesPerRow) { // Create a device-dependent gray color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create a bitmap graphics context with the sample buffer data CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGImageAlphaNone); // Create a Quartz image from the pixel data in the bitmap graphics context CGImageRef quartzImage = CGBitmapContextCreateImage(context); // Free up the context and color space CGContextRelease(context); CGColorSpaceRelease(colorSpace); // Create an image object from the Quartz image UIImage *image = [UIImage imageWithCGImage:quartzImage]; return image; } 上述代码可能会引发这样的疑问:灰度图为什么不须要U和V通道的数据。确实,此问题我最近特地查阅了些资料。建立灰度图时,有些人还将U、V通道在偏置前(值范围[-128, 127])设置为0,或者偏置后(值范围[0, 255])设置为128,然而,建立灰度图时,他们的代码并未使用UV数据。另外,看到一种说法是: Y通道就是平时所说的灰度通道。 固然,以我有限的了解来看,我的不太承认这种说法。缘由是,Y通道是YUV的一个份量,而灰度是复合量,即便数值接近,在概念上应该也是有区别的。数值接近的意思是,以BT. 601转换矩阵为例进行证实: Y = 0.299R + 0.587G + 0.114B GrayScale = (R + G + B) / 3 可见,Y值在数值接近灰度值。下面,对建立图像的代码段进行简要分析。 一些开源项目,如SDWebImage,它使用CGColorSpaceCreateDeviceRGB函数,是由于它的数据源是RGB,而咱们这里的YUV数据须要通过颜色转换矩阵运算才能获得RGB,简单起见,由CGColorSpaceCreateDeviceGray函数建立灰度图可直接看到图像发生的变化,缺点是,丢失了颜色信息。示意图以下所示。 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 生成灰度图 虽然,像素格式为YUV的视频解码后几乎均可生成灰度图。然而,并非全部的图像原始数据都能经过Core Graphics生成可视图像,iOS支持的像素格式很是有限,以下所示。 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 生成灰度图支持的像素格式 五、坑 操做CVImageBuffer(CVPixelBuffer)虽然看着没什么难度,然而,仍是有些大大小小的问题。若是对此不做描述,那么本文档的标题真是太标题党了。下面,给出我在开发过程当中遇到并解决的状况。 5.一、直接操做解码回调的CVImageBuffer(CVPixelBuffer)存在的问题 在解码回调函数中进行YUV处理,不管是否同步解码,或者解码与建立纹理、刷新界面是否为同一线程。须要注意的是,解码回调获得的CVPixelBuffer中的图像是上一次解码回调中处理过的图像,而非视频压缩数据经过解码获得的新的完整图像。换句话说,在一个关键帧解码成功后,其后续P帧之前一帧为基础,继续解码并将结果叠加到新画面,而后传递到解码回调函数。简单示意之。 Decode Thread: VTDecompressionSessionDecodeFrame -> VTDecoderCallback (进行图像处理) -> 添加到待显示队列 Rendering Thread: 读取待显示队列、获得已处理的CVPixelBuffer -> CVOpenGLESTextureCacheCreateTextureFromImage 下面,详细讨论上述状况。进行YUV三个通道处理后,播放出来的画面看着正常,相关资源占用信息以下所示。然而,经输出Video Toolbox回调函数传递过来的CVPixelBuffer或说CVImageBuffer,发现是以前咱们处理过的图像,并在上一关键帧基础上持续叠加P帧,把结果图像做为下一帧视频。 iOS面向编码|iOSVideoToolbox:读写解码回调函数CVImageBufferRef的YUV图像 CPU不超负荷的资源占用 CPU不超负荷的GPU占用 CPU不超负荷的Y通道图 CPU不超负荷的解码回调每帧图像 可见,做为一个关键帧间隔为15的视频序列,src_1.jpg与src_16.jpg因关键帧获得一次当即刷新,随后的图像都在YUV处理的基础上持续叠加。 5.二、CVPixelBuffer上传至GPU后图像垂直镜像问题 对于CMVideoFormatDescription及指定输出的CVPixelBuffer信息以下的解码过程,在自行建立CVPixelBuffer后,将解码回调函数的CVPixelBuffer数据拷贝到新CVPixelBuffer,一般会遇到图像颠倒了,确切地说,图像出现垂直镜像问题。不过,使用前面生成灰度图函数获得的图像都是正的,不存在颠倒,只有上传到GPU里才存在此现象。缘由是,计算机的图像存储时有本身的坐标,这个坐标与OpenGL ES的纹理坐标的Y轴正好相反,故图像在GPU中是颠倒的。 CMVideoFormatDescription { CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; FullRangeVideo = 0; SampleDescriptionExtensionAtoms = { avcC = <01640033 ffe10014 67640033 ac1b4583 c0f68400 000fa000 03a98010 01000468 e923cbfd f8f800>; }; } destinationImageBufferAttributes = { OpenGLESCompatibility = 1; PixelFormatType = 2033463856; } 如今,尝试使用Core Video接口处理此问题。首先,判断源及目标图像是否翻转。 bool isFlipped = CVImageBufferIsFlipped(pixelBuffer);if (isFlipped) { NSLog(@"pixelBuffer is %s", isFlipped ? "flipped" : "not flipped"); } isFlipped = CVImageBufferIsFlipped(imageBuffer);if (isFlipped) { NSLog(@"imageBuffer is %s", isFlipped ? "flipped" : "not flipped"); } 发现图像都是翻转的,执行结果所下。 pixelBuffer is flipped imageBuffer is flipped 显然,还须要更多信息去判断。再获取两个缓冲区的ShouldNotPropagate属性,发现都没有值。可是,回调函数的像素缓冲区有ShouldPropagate属性,而咱们自行建立的缓冲区则无此属性,以下所示。 CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; CVImageBufferColorPrimaries = "SMPTE_C"; CVImageBufferTransferFunction = "ITU_R_709_2"; CVImageBufferYCbCrMatrix = "ITU_R_601_4"; ColorInfoGuessedBy = VideoToolbox; 那么,根据H.264文档,CVFieldCount只是说明CVPixelBuffer只有一个访问单元(Access Unit),而BottomField和TopField两个域表达了图像缓冲区两个色度的位置,与图像倒转无关。其他参数,如YCbCrMatrix只是源视频须要的YUV转RGB矩阵。 因此,根据我对Core Video的了解,目前使用Core Video接口没法处理此状况,只能在GPU中经过镜像纹理坐标或者使用前面介绍的垂直镜像方式解决