前言
在音視頻處理中,如稍微不太注意代碼的實(shí)現(xiàn)方式,可能會(huì)導(dǎo)致內(nèi)存泄漏以及內(nèi)存占用飆升的問(wèn)題,具體問(wèn)題具體分析,來(lái)探究下為什么都能實(shí)現(xiàn)功能的前提下,不同的實(shí)現(xiàn)方式,會(huì)有不同的內(nèi)存收益。
舉例
以給視頻中添加字幕為例,視頻添加字幕通常有兩種方式。一是在layer層添加字幕的layer,二是如水印一樣,將文字渲染到視頻流中。這里,重點(diǎn)說(shuō)的是第二種方式。
視頻幀處理
給視頻加入字幕,實(shí)際上是讀取視頻幀,對(duì)滿足條件的幀進(jìn)行渲染。大致流程如下:

使用AVAssetReader可以獲取到CVSampleBuffer,通過(guò)CVSampleBuffer又可以獲取到CVPixelBufferRef,CVPixelBufferRef 是一種像素圖片類型。
獲取CVPixelBufferRef 后,通常需要將CVPixelBufferRef 轉(zhuǎn)成UIImage。
然后對(duì)UIImage做額外的繪制工作,比如添加文本。
CVSampleBuffer轉(zhuǎn)UIImage有兩種方法
// 基礎(chǔ)參數(shù)
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轉(zhuǎn)成UIImage,但是,兩種方法是基于不同的框架,內(nèi)存消耗上,工作原理上有不同表現(xiàn)。
以處理一分鐘的的視頻為例
1.在使用CVSampleBuffer轉(zhuǎn)UIImage的過(guò)程中,使用方法一也就是CoreVideo+Core Graphics的組合,與直接使用CoreImage并沒(méi)有太大的內(nèi)存出入,也并不能充分體現(xiàn)CoreImage的優(yōu)勢(shì)。
經(jīng)測(cè)試,在iPhone7上,方式一的內(nèi)存峰值為36M,平均值25.6M,方式二的內(nèi)存峰值是26.1M,平均值25.1M。
2.接下來(lái),是對(duì)每一幀的視頻畫(huà)面,添加文本,將計(jì)算好的文本,使用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再次轉(zhuǎn)成CVPixelBufferRef寫(xiě)入到新的視頻文件中
此時(shí),依舊有上面的兩種方式。繼續(xù)使用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 需要轉(zhuǎn)換成需要的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;
}
此時(shí)內(nèi)存會(huì)逐漸從25M -> 上升至650M,好在兩種方式都是基于GPU的。但這肯定是不能接受的了,那,為什么會(huì)這樣?
在這個(gè)過(guò)程中,CoreGrahics都做了什么?
首先,該方式先獲取了Image的尺寸,之后,創(chuàng)建CVPixelBuffer,這些流程其實(shí)都不好內(nèi)存。真正消耗內(nèi)存的方法,是在CVPixelBufferLockBaseAddress()之后,使用CGBitmapContextCreate次方法解碼Image獲取bitmap。bitmap的大小是可以計(jì)算的,以一張寬高1280 * 720的圖,內(nèi)存占用即可達(dá)到 1280 * 720 * 4 (32位RGBA) = 3.5M。
CoreImage代碼演示
// 將一個(gè)處理過(guò)的圖像渲染到 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);
使用這種方式,內(nèi)存消耗會(huì)控制在25M左右,和650M相差了進(jìn)30倍。
保持一個(gè)CIContext的引用,它提供一個(gè)橋梁來(lái)連接我們的Core Image對(duì)象和 OpenGL上下文。我們創(chuàng)建一次就可以一直使用它。這個(gè)上下文允許Core Image在后臺(tái)做優(yōu)化,比如緩存和重用紋理之類的資源等。重要的是這個(gè)上下文我們一直在重復(fù)使用。
CoreImage既可以運(yùn)行在CPU也可以是GPU,區(qū)別在于使用不同的創(chuàng)建方式,及API。
未完......