视频帧处理中的性能问题

前言

在音视频处理中,如稍微不太注意代码的实现方式,可能会导致内存泄漏以及内存占用飙升的问题,具体问题具体分析,来探究下为什么都能实现功能的前提下,不同的实现方式,会有不同的内存收益。

举例

以给视频中添加字幕为例,视频添加字幕通常有两种方式。一是在layer层添加字幕的layer,二是如水印一样,将文字渲染到视频流中。这里,重点说的是第二种方式。

视频帧处理

给视频加入字幕,实际上是读取视频帧,对满足条件的帧进行渲染。大致流程如下:


image.png

使用AVAssetReader可以获取到CVSampleBuffer,通过CVSampleBuffer又可以获取到CVPixelBufferRefCVPixelBufferRef 是一种像素图片类型。
获取CVPixelBufferRef 后,通常需要将CVPixelBufferRef 转成UIImage
然后对UIImage做额外的绘制工作,比如添加文本。

CVSampleBufferUIImage有两种方法
// 基础参数
CMSampleBufferRef buffer =....
__block CVImageBufferRef CVPixelBuffer = CMSampleBufferGetImageBuffer(buffer);

// 方式一

- (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    if (!sampleBuffer) {
        return nil;
    }
    // Get a CMSampleBuffer's Core Video image buffer for the media data
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // Lock the base address of the pixel buffer
    CVPixelBufferLockBaseAddress(imageBuffer, 0);

    // Get the number of bytes per row for the pixel buffer
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);

    // Get the number of bytes per row for the pixel buffer
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    // Get the pixel buffer width and height
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);

    // Create a device-dependent RGB color space
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // Create a bitmap graphics context with the sample buffer data
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
                                                 bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    // Create a Quartz image from the pixel data in the bitmap graphics context
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);


    // Unlock the pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    // Free up the context and color space
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);

    // Create an image object from the Quartz image
//    UIImage *image = [UIImage imageWithCGImage:quartzImage];
    UIImage *image = [UIImage imageWithCGImage:quartzImage scale:1.0f orientation:UIImageOrientationUp];
    // Release the Quartz image
    CGImageRelease(quartzImage);

    return image;

}

方式二

- (UIImage*)imageFromSampleBuffer:(CVPixelBufferRef)p {
    CIImage* ciImage = [CIImage imageWithCVPixelBuffer:p];

    CIContext* context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)}];

    CGRect rect = CGRectMake(0, 0, CVPixelBufferGetWidth(p), CVPixelBufferGetHeight(p));
    CGImageRef videoImage = [context createCGImage:ciImage fromRect:rect];

    UIImage* image = [UIImage imageWithCGImage:videoImage];
    CGImageRelease(videoImage);

    return image;
}

两种方式,都可以将CVSampleBuffer转成UIImage,但是,两种方法是基于不同的框架,内存消耗上,工作原理上有不同表现。

以处理一分钟的的视频为例

1.在使用CVSampleBufferUIImage的过程中,使用方法一也就是CoreVideo+Core Graphics的组合,与直接使用CoreImage并没有太大的内存出入,也并不能充分体现CoreImage的优势。
经测试,在iPhone7上,方式一的内存峰值为36M,平均值25.6M,方式二的内存峰值是26.1M,平均值25.1M

2.接下来,是对每一帧的视频画面,添加文本,将计算好的文本,使用Core Graphics渲染到每一帧上。
代码示例如下:

- (UIImage*)addText:(NSString*)text addToView:(UIImage*)image{
    
    int w = image.size.width;
    int h = image.size.height;
    
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, w, h)];
    
    NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
    textStyle.lineBreakMode = NSLineBreakByWordWrapping;
    textStyle.alignment = NSTextAlignmentCenter;//水平居中
    UIFont* font = [UIFont systemFontOfSize:40];
    
    NSDictionary *attr = @{NSFontAttributeName: font, NSForegroundColorAttributeName : [UIColor whiteColor], NSParagraphStyleAttributeName:textStyle,NSKernAttributeName:@(2),NSBackgroundColorAttributeName:[UIColor orangeColor]};
        
    [text drawInRect:CGRectMake(0, h - 240, w, 60) withAttributes:attr];
    
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return newImage;
}

3.将UIImage再次转成CVPixelBufferRef写入到新的视频文件中

此时,依旧有上面的两种方式。继续使用Core Gragrahic+CoreVide的方式和CIImage

方式一的代码示例如下:

//CGImageRef --> CVPixelBufferRef
- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image {
    
    CGSize frameSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image));
    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:NO], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:NO], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];
    CVPixelBufferRef pxbuffer = NULL;
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width,
                                          frameSize.height,  kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options,
                                          &pxbuffer);
    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    // kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst 需要转换成需要的32BGRA空间
    CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width,
                                                 frameSize.height, 8, 4*frameSize.width, rgbColorSpace,
                                                 kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
                                           CGImageGetHeight(image)), image);
    
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    
    return pxbuffer;
}

此时内存会逐渐从25M -> 上升至650M,好在两种方式都是基于GPU的。但这肯定是不能接受的了,那,为什么会这样?

在这个过程中,CoreGrahics都做了什么?

首先,该方式先获取了Image的尺寸,之后,创建CVPixelBuffer,这些流程其实都不好内存。真正消耗内存的方法,是在CVPixelBufferLockBaseAddress()之后,使用CGBitmapContextCreate次方法解码Image获取bitmapbitmap的大小是可以计算的,以一张宽高1280 * 720的图,内存占用即可达到 1280 * 720 * 4 (32位RGBA) = 3.5M。

CoreImage代码演示

// 将一个处理过的图像渲染到 pixelBuffer
CIImage *result = [CIImage imageWithCGImage:image_text.CGImage];
CVPixelBufferLockBaseAddress(CVPixelBuffer, 0);
CGColorSpaceRef cSpace = CGColorSpaceCreateDeviceRGB();
[self.ciContext render:result toCVPixelBuffer:CVPixelBuffer bounds:result.extent colorSpace:cSpace];
CVPixelBufferUnlockBaseAddress(CVPixelBuffer, 0);

使用这种方式,内存消耗会控制在25M左右,和650M相差了进30倍。
保持一个CIContext的引用,它提供一个桥梁来连接我们的Core Image对象和 OpenGL上下文。我们创建一次就可以一直使用它。这个上下文允许Core Image在后台做优化,比如缓存和重用纹理之类的资源等。重要的是这个上下文我们一直在重复使用。
CoreImage既可以运行在CPU也可以是GPU,区别在于使用不同的创建方式,及API。

未完......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容