本篇教程的目标是使用 UIImageView 渲染视频,因为这是大多数 iOS 开发者最熟悉的方式,也是容易想到的方式,确定了渲染View之后,那么就要首先想办法准备帧数据了,我会把之前缓存的 avframe 改成缓存 UIImage,然后准备一个 UIImageView 当成 render 。
我找到了两种方法,都可以将 avframe 转成 UIImage ,有意思的是差别还蛮大的!这是我测试之后的数据:
缓存20s的视频包;缓存2s的解码帧;
when USEBITMAP 1
22fps GPU 12% CPU 55% Memory 110M
when USEBITMAP 0
22fps GPU 14% CPU 50% Memory 26M
可以修改 USEBITMAP 的值进行对比!
注:一定不要使用模拟器去查看这些参数,因为模拟器是通过 x86_64 架构的电脑模拟出来的环境和真实环境相差特别大!
由于大部分逻辑都是基于第三天是相同的,所以直接贴下核心代码吧:
开始读包前设置下转换格式,准备下渲染 render
// 渲染View
if (!self.renderView) {
UIImageView *render = [[UIImageView alloc]initWithFrame:self.view.bounds];
[self.view addSubview:render];
render.contentMode = UIViewContentModeScaleAspectFit;
render.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.renderView = render;
}
#if USEBITMAP
enum AVPixelFormat pix_fmt = PIX_FMT_RGB24;
#else
enum AVPixelFormat pix_fmt = PIX_FMT_NV12;
#endif
const int picSize = avpicture_get_size(pix_fmt, self.vwidth, self.vheight);
self.out_buffer = malloc(picSize);
self.img_convert_ctx = sws_getContext(self.vwidth, self.vheight, self.format, self.vwidth, self.vheight, pix_fmt, SWS_BICUBIC, NULL, NULL, NULL);
self.pFrameYUV = av_frame_alloc();
avpicture_fill((AVPicture *)self.pFrameYUV, self.out_buffer, pix_fmt, self.vwidth, self.vheight);
帧格式转换方法
- (UIImage *)imageFromAVFrame:(AVFrame*)video_frame w:(int)w h:(int)h
{
#if USEBITMAP
return [MRConvertUtil imageFromAVFrameV2:video_frame w:self.vwidth h:self.vheight];
#else
return [MRConvertUtil imageFromAVFrame:video_frame w:self.vwidth h:self.vheight];
#endif
}
解码线程逻辑修改
// 根据配置把数据转换成 NV12 或者 RGB24
int pictRet = sws_scale(self.img_convert_ctx, (const uint8_t* const*)video_frame->data, video_frame->linesize, 0, self.vheight, self.pFrameYUV->data, self.pFrameYUV->linesize);
if (pictRet <= 0) {
av_frame_free(&video_frame);
return ;
}
UIImage *img = [self imageFromAVFrame:self.pFrameYUV w:self.vwidth h:self.vheight];
// 获取时长
const double frameDuration = av_frame_get_pkt_duration(video_frame) * self.videoTimeBase;
av_frame_free(&video_frame);
// 构造模型
MRVideoFrame *frame = [[MRVideoFrame alloc]init];
frame.duration = frameDuration;
frame.image = img;
渲染
- (void)displayVideoFrame:(MRVideoFrame *)frame
{
UIImage *image = frame.image;
[self.renderView setImage:image];
}
完整逻辑可打开工程查看运行。
对前面贴出来的数据做个简单分析:
- 当 USEBITMAP 1时,逻辑走了如下代码
+ (UIImage *)imageFromAVFrameV2:(AVFrame*)video_frame w:(int)w h:(int)h
{
const UInt8 *rgb = video_frame->data[0];
size_t bytesPerRow = video_frame->linesize[0];
CFIndex length = bytesPerRow*h;
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
///需要copy!因为video_frame是重复利用的;里面的数据会变化!
CFDataRef data = CFDataCreate(kCFAllocatorDefault, rgb, length);
CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGImageRef cgImage = CGImageCreate(w,
h,
8,
24,
bytesPerRow,
colorSpace,
bitmapInfo,
provider,
NULL,
NO,
kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace);
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
CGDataProviderRelease(provider);
CFRelease(data);
return image;
}
具体流程是先将 avframe 的像素格式转成 RGB24,然后通过 CoreGraphics 框架创建一副位图,然后转成 UIImage,因为这一过程完全没有 GPU 的参与,数据也只是内存之间的 copy ,所以导致了内存和CPU占用都比较高!处理流程相对简单!
- 当 USEBITMAP 0时,逻辑走了如下代码
+ (CVPixelBufferRef)pixelBufferFromAVFrame:(AVFrame*)pFrame w:(int)w h:(int)h
{
// NV12数据转成 UIImage
//YUV(NV12)-->CIImage--->UIImage Conversion
NSDictionary *pixelAttributes = @{(NSString*)kCVPixelBufferIOSurfacePropertiesKey:@{}};
CVPixelBufferRef pixelBuffer = NULL;
CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault,
w,
h,
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
(__bridge CFDictionaryRef)(pixelAttributes),
&pixelBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer,0);
unsigned char *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
// Here y_ch0 is Y-Plane of YUV(NV12) data.
unsigned char *y_ch0 = pFrame->data[0];
unsigned char *y_ch1 = pFrame->data[1];
memcpy(yDestPlane, y_ch0, w * h);
unsigned char *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
// Here y_ch1 is UV-Plane of YUV(NV12) data.
memcpy(uvDestPlane, y_ch1, w * h / 2.0);
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
if (result != kCVReturnSuccess) {
NSLog(@"Unable to create cvpixelbuffer %d", result);
}
return pixelBuffer;
}
+ (UIImage *)imageFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer w:(int)w h:(int)h
{
// CIImage Conversion
CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *context = [CIContext contextWithOptions:nil];
///引发内存泄露? https://stackoverflow.com/questions/32520082/why-is-cicontext-createcgimage-causing-a-memory-leak
NSTimeInterval begin = CFAbsoluteTimeGetCurrent();
CGImageRef cgImage = [context createCGImage:coreImage
fromRect:CGRectMake(0, 0, w, h)];
NSTimeInterval end = CFAbsoluteTimeGetCurrent();
// UIImage Conversion
UIImage *uiImage = [[UIImage alloc] initWithCGImage:cgImage
scale:1.0
orientation:UIImageOrientationUp];
NSLog(@"decode an image cost :%g",end-begin);
CVPixelBufferRelease(pixelBuffer);
CGImageRelease(cgImage);
return uiImage;
}
+ (UIImage *)imageFromAVFrame:(AVFrame*)video_frame w:(int)w h:(int)h
{
CVPixelBufferRef pixelBuffer = [self pixelBufferFromAVFrame:video_frame w:w h:h];
if (pixelBuffer) {
return [self imageFromCVPixelBuffer:pixelBuffer w:w h:h];
}
return nil;
}
具体流程是先将 avframe 的 YUV 像素格式转成 NV12(YUV 420sp) 排列形式,大多数视频像素格式是 YUV 420P,这一过程转换一般是比较快的(YUV 转 RGB 的运算量比这个大些应该会稍慢) 然后通过 CoreVideo 框架创建一个 CVPixelBufferRef ,然后创建 CIImage 对象,接着把 CIImage 转 CGImageRef,最后 CGImageRef 转成 UIImage !
是不是觉得饶了一大圈啊?我当时也是这么觉得,因为使用 UIImageView 渲染的,所以无论如何最后都要转成 UIImage 帧才行!but,就是因为绕这么一圈,神奇的事情发生了,竟然把内存占用降下来了,这一过程应该是有 GPU 参与的,数据是如何 copy 的也有待进一步去证实。处理流程相对复杂!
在第二天的教程中我有提到过她,YUV 是视频像素格式的一种,YUV 是最流行的,一般要比 RGB 方式占用的内存空间要少些!是否会少,少了多少跟 YUV 数据的排列有关,一般情况下,使用 YUV 420p的最多,这种方式比使用 RGB24 要少一半的空间!
YUV 主要用于电视上,它将亮度和色彩信息相分离,当只有 Y (Luma) 没有 UV 时,也是可以显示完整图像的,具体什么样?看过黑白电视吧,那就是!是不是瞬间感觉好牛逼的格式啊,居然能同时兼容黑白电视和彩色电视!