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. CMSampleTimingInfo timing = {kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid};
  10. //获取视频信息
  11. CMVideoFormatDescriptionRef videoInfo = NULL;
  12. OSStatus result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);
  13. NSParameterAssert(result == 0 && videoInfo != NULL);
  14. CMSampleBufferRef sampleBuffer = NULL;
  15. result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);
  16. NSParameterAssert(result == 0 && sampleBuffer != NULL);
  17. CFRelease(pixelBuffer);
  18. 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渲染就可以了。

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

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

  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. <strong>    @synchronized(self) {
  7. if (self.previousPixelBuffer){
  8. CFRelease(self.previousPixelBuffer);
  9. self.previousPixelBuffer = nil;
  10. }
  11. self.previousPixelBuffer = CFRetain(pixelBuffer);
  12. }</strong>
  13. ...........略去其他
  14. }

其次,当切后台事件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. - (void) setupPlayerBackgroundImage{
  6. if (self.isVideoHWDecoderEnable){
  7. @synchronized(self) {
  8. if (self.previousPixelBuffer){
  9. self.image = [self getUIImageFromPixelBuffer:self.previousPixelBuffer];
  10. CFRelease(self.previousPixelBuffer);
  11. self.previousPixelBuffer = nil;
  12. }
  13. }
  14. }
  15. }

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

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

  1. [[AVSampleBufferDisplayLayer alloc] init]

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

AVSampleBufferDisplayLayer----转的更多相关文章

  1. iOS8系统H264视频硬件编解码说明

    公司项目原因,接触了一下视频流H264的编解码知识,之前项目使用的是FFMpeg多媒体库,利用CPU做视频的编码和解码,俗称为软编软解.该方法比较通用,但是占用CPU资源,编解码效率不高.一般系统都会 ...

  2. 第11月第3天 直播 rtmp yuv

    1. LiveVideoCoreSDK AudioUnitRender ==> MicSource::inputCallback ==> GenericAudioMixer::pushBu ...

  3. 02:H.264学习笔记

    H.264组成 1.网络提取层 (Network Abstraction Layer,NAL) 2.视讯编码层 (Video Coding Layer,VCL) a.H.264/AVC影像格式阶层架构 ...

  4. 01:***VideoToolbox硬编码H.264

    最近接触了一些视频流H264的编解码知识,之前项目使用的是FFMpeg多媒体库,利用CPU做视频的编码和解码,俗称为软编软解.该方法比较通用,但是占用CPU资源,编解码效率不高.一般系统都会提供GPU ...

  5. Direct Access to Video Encoding and Decoding

    来源:http://asciiwwdc.com/2014/sessions/513   Direct Access to Video Encoding and Decoding  Session 5 ...

  6. How to decode a H.264 frame on iOS by hardware decoding?

    来源:http://stackoverflow.com/questions/25197169/how-to-decode-a-h-264-frame-on-ios-by-hardware-decodi ...

  7. How to use VideoToolbox to decompress H.264 video stream

    来源:http://stackoverflow.com/questions/29525000/how-to-use-videotoolbox-to-decompress-h-264-video-str ...

随机推荐

  1. BaseAdapter日常的封装

    我们日常开发中BaseAdapter使用非常的多,大家有没有想过,再进一步封装,将平常使用的对象集合,利用泛型放入集合中,再也不用每次都重写那几个方法了,当然我也提供我的下载地址(https://gi ...

  2. LCD接口(转载)

    LCD接口分类 1.   I8080接口,我觉得应该就是所谓的8080,通常会用在12864屏上面,且有内部sdram,不需要实时的刷新图片,速度有限制, 支持的数据宽度有8/9/16/18bit,接 ...

  3. 此操作失败的原因是对 IID 为“{000208DA-0000-0000-C000-000000000046}”的接口的 COM 组件调用 QueryInterface

    有些电脑报错,有些电脑正常. 环境:VS2010 WinForm程序, Office2007 C#操作Excel时报错.错误: 无法将类型为“System.__ComObject”的 COM 对象强制 ...

  4. 运用node的文件系统模块批量修改文件名

      如果我们需要大批量修改一个文件中的名称,比如,删除文件名中的副本时,就可以借助node的文件系统模块,快捷快速的完成. 首先建立一个js文件(changeName.js),代码如下: // 引入f ...

  5. git push :推送本地更改到远程仓库的三种模式

    摘要:由于在git push过程中,no-fast-forward 的push会被拒绝,如何解决git push失败的问题?这里面有三种方法,分别会形成merge形式的提交历史,线性形式的提交历史,覆 ...

  6. linux复制指定目录下的全部文件到另一个目录中

    linux复制指定目录下的全部文件到另一个目录中复制指定目录下的全部文件到另一个目录中文件及目录的复制是经常要用到的.linux下进行复制的命令为cp.假设复制源目录 为 dir1 ,目标目录为dir ...

  7. 阿里yum源

    转:http://mirrors.aliyun.com/help/centos?spm=5176.bbsr150321.0.0.d6ykiD 1.备份 mv /etc/yum.repos.d/Cent ...

  8. Thinkphp框架回顾(三)之怎么实现平常的sql操作数据库

    1.首先简单介绍一下我们的数据库,thinkphp数据库下有一个tp_user表,然后有四个字段....id,username,password,sex 我们今天的任务就是在Thinkphp下将数据调 ...

  9. Discuz的安装 (原创帖,转载请注明出处)

    ========================写在前面的话========================= 1.LAMP环境搭建请查看这篇日志:http://www.cnblogs.com/yic ...

  10. 蓝屏 Dump文件分析方法

    WinDbg使用有点麻烦,还要符号表什么的.试了下,感觉显示很乱,分析的也不够全面... 试试其他的吧!今天电脑蓝屏了,就使用其dump文件测试,如下: 1.首先,最详细的,要属Osr Online这 ...