前言
在音视频处理中,如稍微不太注意代码的实现方式,可能会导致内存泄漏以及内存占用飙升的问题,具体问题具体分析,来探究下为什么都能实现功能的前提下,不同的实现方式,会有不同的内存收益。
举例
以给视频中添加字幕为例,视频添加字幕通常有两种方式。一是在layer
层添加字幕的layer
,二是如水印一样,将文字渲染到视频流中。这里,重点说的是第二种方式。
视频帧处理
给视频加入字幕,实际上是读取视频帧,对满足条件的帧进行渲染。大致流程如下:
使用AVAssetReader
可以获取到CVSampleBuffer
,通过CVSampleBuffer
又可以获取到CVPixelBufferRef
,CVPixelBufferRef
是一种像素图片类型。
获取CVPixelBufferRef
后,通常需要将CVPixelBufferRef
转成UIImage
。
然后对UIImage
做额外的绘制工作,比如添加文本。
CVSampleBuffer
转UIImage
有两种方法
// 基础参数
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.在使用CVSampleBuffer
转UIImage
的过程中,使用方法一也就是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
获取bitmap。bitmap
的大小是可以计算的,以一张宽高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。
未完......