• AVSampleBufferDisplayLayer----转


    http://blog.csdn.net/fernandowei/article/details/52179631

    目前大多数iOS端的视频渲染都使用OpenGLES,但如果仅仅为了渲染而不做其他的例如美颜等效果,其实可以使用iOS8.0新出的AVSampleBufferDisplayLayer。对AVSampleBufferDisplayLayer,官方说明中有一句话,“The AVSampleBufferDisplayLayer class is a subclass of CALayer that displays compressed or uncompressed video frames.”,即AVSampleBufferDisplayLayer既可以用来渲染解码后的视频图片,也可以直接把未解码的视频帧送给它,完成先解码再渲染出去的步骤。

    由于本人在使用AVSampleBufferDisplayLayer之前已经videotoolbox中相关api完成了h264视频的硬解,所以这里仅仅使用AVSampleBufferDisplayLayer来渲染,即送给它pixelBuffer。

    个人选择了UIImageView作为渲染的view(没有直接使用UIView的原因后面会提到),而且也没有重载UIView的layerClass函数来使AVSampleBufferDisplayLayer成为这个view的默认layer(不这么做的原因后面提到)。

    具体做法,首先,建立AVSampleBufferDisplayLayer并把它添加成为当前view的子layer:

     
    1. self.sampleBufferDisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];  
    2. self.sampleBufferDisplayLayer.frame = self.bounds;  
    3. self.sampleBufferDisplayLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));  
    4. self.sampleBufferDisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;  
    5. self.sampleBufferDisplayLayer.opaque = YES;  
    6. [self.layer addSublayer:self.sampleBufferDisplayLayer];  

    其次,把得到的pixelbuffer包装成CMSampleBuffer并设置时间信息:

     
    1. //把pixelBuffer包装成samplebuffer送给displayLayer  
    2. - (void)dispatchPixelBuffer:(CVPixelBufferRef) pixelBuffer  
    3. {  
    4.     if (!pixelBuffer){  
    5.         return;  
    6.     }  
    1. @synchronized(self) {  
    2.     if (self.previousPixelBuffer){  
    3.         CFRelease(self.previousPixelBuffer);  
    4.         self.previousPixelBuffer = nil;  
    5.     }  
    6.     self.previousPixelBuffer = CFRetain(pixelBuffer);  
    7. }  
    8.   
    9. //不设置具体时间信息  
    10. CMSampleTimingInfo timing = {kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid};  
    11. //获取视频信息  
    12. CMVideoFormatDescriptionRef videoInfo = NULL;  
    13. OSStatus result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);  
    14. NSParameterAssert(result == 0 && videoInfo != NULL);  
    15.   
    16. CMSampleBufferRef sampleBuffer = NULL;  
    17. result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);  
    18. NSParameterAssert(result == 0 && sampleBuffer != NULL);  
    19. CFRelease(pixelBuffer);  
    20. CFRelease(videoInfo);    
     
    1. CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);  
    2. CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);  
    3. CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);  
    4. [self enqueueSampleBuffer:sampleBuffer toLayer:self.sampleBufferDisplayLayer];  
    5. CFRelease(sampleBuffer);  

    这里不设置具体时间信息且设置kCMSampleAttachmentKey_DisplayImmediately为true,是因为这里只需要渲染不需要解码,所以不必根据dts设置解码时间、根据pts设置渲染时间。

    最后,数据送给AVSampleBufferDisplayLayer渲染就可以了。

    [objc] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. <p class="p1"><pre name="code" class="objc">- (void)enqueueSampleBuffer:(CMSampleBufferRef) sampleBuffer toLayer:(AVSampleBufferDisplayLayer*) layer  
    2. {  
    3.     if (sampleBuffer){  
    4.         CFRetain(sampleBuffer);  
    5.         [layer enqueueSampleBuffer:sampleBuffer];  
    6.         CFRelease(sampleBuffer);  
    7.         if (layer.status == AVQueuedSampleBufferRenderingStatusFailed){  
    8.             NSLog(@"ERROR: %@", layer.error);  
    9.             if (-11847 == layer.error.code){  
    10.                 [self rebuildSampleBufferDisplayLayer];  
    11.             }  
    12.         }else{  
    13. //            NSLog(@"STATUS: %i", (int)layer.status);  
    14.         }  
    15.     }else{  
    16.         NSLog(@"ignore null samplebuffer");  
    17.     }  
    18. }  

    可以看到,使用AVSampleBufferDisplayLayer进行视频渲染比使用OpenGLES简单了许多。不过遗憾的是,这里有一个iOS系统级的bug,AVSampleBufferDisplayLayer会在遇到后台事件等一些打断事件时失效,即如果视频正在渲染,这个时候摁home键或者锁屏键,再回到视频的渲染界面,就会显示渲染失败,错误码就是上述代码中的-11847。

    个人在遇到上述问题后,联想到之前使用videotoolbox解码视频时遇到类似后台事件时VTDecompressionSession会失效从而需要撤销当前VTDecompressionSession来重新建立VTDecompressionSession的过程,在AVSampleBufferDisplayLayer失效时,也去撤销当前这个AVSampleBufferDisplayLayer再重建一个;这里说到之前卖的一个关子,如果这个AVSampleBufferDisplayLayer是view的默认layer,这时就没法只撤销layer而不动view,所以把AVSampleBufferDisplayLayer作为view的子layer更方便,撤销重建的过程如下:

    1. - (void)rebuildSampleBufferDisplayLayer{  
    2.     @synchronized(self) {  
    3.         [self teardownSampleBufferDisplayLayer];  
    4.         [self setupSampleBufferDisplayLayer];  
    5.     }  
    6. }  
    7.   
    8. - (void)teardownSampleBufferDisplayLayer  
    9. {  
    10.     if (self.sampleBufferDisplayLayer){  
    11.         [self.sampleBufferDisplayLayer stopRequestingMediaData];  
    12.         [self.sampleBufferDisplayLayer removeFromSuperlayer];  
    13.         self.sampleBufferDisplayLayer = nil;  
    14.     }  
    15. }  
    16.   
    17. - (void)setupSampleBufferDisplayLayer{  
    18.     if (!self.sampleBufferDisplayLayer){  
    19.         self.sampleBufferDisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];  
    20.         self.sampleBufferDisplayLayer.frame = self.bounds;  
    21.         self.sampleBufferDisplayLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));  
    22.         self.sampleBufferDisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;  
    23.         self.sampleBufferDisplayLayer.opaque = YES;  
    24.         [self.layer addSublayer:self.sampleBufferDisplayLayer];  
    25.     }else{  
    26.         [CATransaction begin];  
    27.         [CATransaction setDisableActions:YES];  
    28.         self.sampleBufferDisplayLayer.frame = self.bounds;  
    29.         self.sampleBufferDisplayLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));  
    30.         [CATransaction commit];  
    31.     }  
    32.     [self addObserver];  
    33. }    

    当然,需要监听后台事件,如下:

     
    1. - (void)addObserver{  
    2.     if (!hasAddObserver){  
    3.         NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter];  
    4.         [notificationCenter addObserver: self selector:@selector(didResignActive) name:UIApplicationWillResignActiveNotification object:nil];  
    5.         [notificationCenter addObserver: self selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];  
    6.         hasAddObserver = YES;  
    7.     }  
    8. }  


    做到这里,基本的问题都解决了,视频可以正常渲染了;不过还有一个稍令人不悦的小问题,即app被切到后台再切回来时,由于这个时候AVSampleBufferDisplayLayer已经失效,所以这个时候渲染的view会是黑屏,这会有一到两秒的时间,直到layer重新建立好并开始渲染。那怎么让这个时候不出现黑屏呢?就需要前面提到的UIImageView,做法如下:

     首先,对于每个到来的pixelbuffer,要保留它直到下一个pixelbuffer到来,如下函数中粗体所示:

    1. - (void)dispatchPixelBuffer:(CVPixelBufferRef) pixelBuffer  
    2. {  
    3.     if (!pixelBuffer){  
    4.         return;  
    5.     }  
    6.       
    7. <strong>    @synchronized(self) {  
    8.         if (self.previousPixelBuffer){  
    9.             CFRelease(self.previousPixelBuffer);  
    10.             self.previousPixelBuffer = nil;  
    11.         }  
    12.         self.previousPixelBuffer = CFRetain(pixelBuffer);  
    13.     }</strong>  
    14.     ...........略去其他  
    15. }  

    其次,当切后台事件resignActive事件到来时,用当前最新保存的pixelbuffer去设置UIImageView的image,当然pixelbuffer要先转化成UIImage,方法如下:

    1. - (UIImage*)getUIImageFromPixelBuffer:(CVPixelBufferRef)pixelBuffer  
    2. {  
    3.     UIImage *uiImage = nil;  
    4.     if (pixelBuffer){  
    5.         CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];  
    6.         uiImage = [UIImage imageWithCIImage:ciImage];  
    7.         UIGraphicsBeginImageContext(self.bounds.size);  
    8.         [uiImage drawInRect:self.bounds];  
    9.         uiImage = UIGraphicsGetImageFromCurrentImageContext();  
    10.         UIGraphicsEndImageContext();  
    11.     }  
    12.     return uiImage;  
    13. }  

    然后在resignActive事件处理函数中,设置UIImageView的image,如下:

     
    1. - (void)didResignActive{  
    2.     NSLog(@"resign active");  
    3.     [self setupPlayerBackgroundImage];  
    4. }  
    5.   
    6. - (void) setupPlayerBackgroundImage{  
    7.     if (self.isVideoHWDecoderEnable){  
    8.         @synchronized(self) {  
    9.             if (self.previousPixelBuffer){  
    10.                 self.image = [self getUIImageFromPixelBuffer:self.previousPixelBuffer];  
    11.                 CFRelease(self.previousPixelBuffer);  
    12.                 self.previousPixelBuffer = nil;  
    13.             }  
    14.         }  
    15.     }  
    16. }  

    这样,切完后台回来前台,在layer还没有重新建立好之前,看到的就是设置的UIImageView的image而不是黑屏了,而这个image就是切后台开始时渲染的最后一帧画面。 

    对于前面说到的AVSampleBufferDisplayLayer失效后重建导致的黑屏时间,个人通过验证发现,如果这个重建动作,即下面这句代码,

    1. [[AVSampleBufferDisplayLayer alloc] init]  

    发生在app刚从后台会到前台就会非常耗时,接近两秒,而如果是正在前台正常播放的过程中执行这句话,只需要十几毫秒;前者如此耗时的原因,经过请教其他iOS开发的同事,可能是这个时候系统优先恢复整个app的UI,其他操作被delay;

    提高技能如同提升自信心。
  • 相关阅读:
    Linux常用命令
    杀死进程端口
    Spring boot项目的打包发布
    关于xshell和文件传输相关
    Windows10开发环境搭建
    Windows10设置系统参数
    windows10禁止更新
    Postgresql9.6基础使用(Windows 解压版)
    (转载)Thingsboard入门教程:本地环境搭建和源码编译安装,献给thingsboard编译失败的同学,教程不断完善中,文章最后是thingsboard常见编译失败的问题总结
    系统同时安装 Open JDK and Oracle JDK(Ubuntu16.04)
  • 原文地址:https://www.cnblogs.com/chims-liu-touch/p/5799716.html
Copyright © 2020-2023  润新知