Skip to content

Latest commit

 

History

History
221 lines (168 loc) · 8.7 KB

004.md

File metadata and controls

221 lines (168 loc) · 8.7 KB

使用 UIImageView 渲染视频

本篇教程的目标是使用 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 是视频像素格式的一种,YUV 是最流行的,一般要比 RGB 方式占用的内存空间要少些!是否会少,少了多少跟 YUV 数据的排列有关,一般情况下,使用 YUV 420p的最多,这种方式比使用 RGB24 要少一半的空间!

YUV 主要用于电视上,它将亮度和色彩信息相分离,当只有 Y (Luma) 没有 UV 时,也是可以显示完整图像的,具体什么样?看过黑白电视吧,那就是!是不是瞬间感觉好牛逼的格式啊,居然能同时兼容黑白电视和彩色电视!