C语言iOS Video Toolbox:读写解码回调函数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++式继承,在是不发了多怀疑。

1、读取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到一个缓冲区。

2、写入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
2、iPhone 6p等真机上有的题材:

1、当使用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失败。

2、决定CVPixelBufferCreate创建的格式是该参数pixelFormatType,而不参数pixelAttributes使用kCVPixelBufferPixelFormatTypeKey指定的如素格式。

下介绍部分简便的图像处理办法。

原始灰度图

(一)水平镜像
水平镜像就是图像绕图像中垂直线交换左右像素点位置,使用矩阵运行表示也:

[x, y, 1]   -1    0 0 -> [x', y', 1]
            0     1 0
            width 0 1 

对于CPU而言,矩阵运行通常没有GPU快,因为GPU做2×2、3×3相当于矩阵运算是硬件加速实现的,很可能就是同等长指令处理完毕,而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)];
    }
}

水平镜像

(二)垂直镜像
垂直镜像就是图像绕图像中水平线交换上下像素点位置,使用矩阵运行表示也:

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

垂直镜像

3、CVPixelBufferPool内存池

活动创建CVPixelBufferPool且经过CVPixelBufferPool创建CVPixelBuffer,容易并发CVPixelBuffer被错释放或奇怪加引用计数导致内存泄露,以ijkplayer为条例演示CVPixelBubffer泄露的情况。

CVPixelBuffer泄露

CVPixelBuffer结束引用时引用计数不为0导致内存泄露

万一活动创建CVPixelBuffer,则容易出现内存暴涨问题,如创建一个960×480的YUV420SP格式的CVPixelBuffer所占有内存为700差不多M,如果是异步解码且并未犯内存大小限制,将导致应用崩溃。

CVPixelBufferCreate占用的内存

若果未思自行创建CVPixelBufferPool,也未思量协调创立CVPixelBuffer,取巧的章程是,使用解码回调函数的CVPixelBuffer,则任需担心内存消耗问题。在执行进程被,图像处理后立即编码,这样以的场地不会见招致解码器自身的缓存队列数据出现图像紊乱。前提是,修改后底像素数量在原数的丰厚高克外。当然,这吗会见并发数问题,具体在文档后续有开展座谈

对此解码->图像处理->编码流程,且处理后底图像以及原图像大小不同,则开创编码器时重创CVPixelBufferPool,让系统管理CVPixelBuffer也是可靠的做法。

另外,在图像处理过程中,Video
Toolbox无论指定FullRange还是VideoRange,由此经过Core
Graphics创建RGB图像是毋庸置疑的,和QuickTime播放时画面保持一致。然而,解码出来的YUV420SP数据通过拷贝,接着进行图像处理,存在一些区域颜色有误。通过点名Video
Toolbox输出YUV420P,再进行图像处理则无颜色异常问题。当然,使用的算法为移相应的YUV420P算法,因为个人觉得,这不过生或是我们组织的YUV420SP拷贝及操作算法有误。

4、CVPixelBuffer通过Core Graphics创建灰度图

改了YUV数据后,如果每次都用GPU实现YUV转换RGB,这正如费心,特别是转码等离线计算场合。下面,介绍一种植实现CVPixelBuffer生成UIImage的方,只使Y平面生成图像,判断图像成像方面的处理结果是否顺应预期。

// baseAddress为Y平面地址,传递yuv420(s)p完整数据地址,则忽略uv
UIImage* 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函数创建灰度图可径直看看图像来的变迁,缺点是,丢失了颜色信息。示意图如下所示。

变更灰度图

虽,像素格式为YUV的视频解码后几都不过特别成灰度图。然而,并无是怀有的图像原始数据还能够通过Core
Graphics生成可看图像,iOS支持之比如素格式非常有限,如下所示。

转变灰度图支持之如素格式

5、坑

操作CVImageBuffer(CVPixelBuffer)虽然看正在没什么难度,然而,还是小大大小小的题目。如果对这个不作描述,那么以文档的标题真是极标题党了。下面,给有我以出进程遭到相见并缓解的情。

5.1、直接操作解码回调的CVImageBuffer(CVPixelBuffer)存在的问题

在解码回调函数中开展YUV处理,无论是否同步解码,或者解码与创造纹理、刷新界面是否也同一线程。需要小心的凡,解码回调得到的CVPixelBuffer中之图像是高达亦然次解码回调中拍卖过的图像,而无视频压缩数据经过解码得到的新的完好图像。换句话说,在一个生死攸关帧解码成功后,其继续P帧以前一帧为根基,继续解码并将结果叠加至新画面,然后传递到解码回调函数。简单示意之。

Decode Thread: VTDecompressionSessionDecodeFrame -> VTDecoderCallback (进行图像处理) -> 添加到待显示队列
Rendering Thread: 读取待显示队列、得到已处理的CVPixelBuffer -> CVOpenGLESTextureCacheCreateTextureFromImage

脚,详细谈论上述情况。进行YUV三只通道处理后,广播出来的镜头看在常规,相关资源占用信息如下所示。然而,经输出Video
Toolbox回调函数传递过来的CVPixelBuffer或说CVImageBuffer,发现凡是前我们处理了之图像,并于上同样关键帧基础及频频叠加P帧,把结果图像作为下一帧视频。

CPU不过分的资源占用

CPU不过分的GPU占用

CPU不过分的Y通道图

CPU不过分的解码回调每帧图像

可见,作为一个主要帧间隔为15底视频序列,src_1.jpg与src_16.jpg坐要帧得到相同不行眼看刷新,随后的图像都于YUV处理的根基及不停叠加。

5.2、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中通过镜像纹理坐标或者下前介绍的垂直镜像方式缓解。

参照和推介阅读

  • Create CVPixelBuffer from YUV with IOSurface
    backed