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;

最新文章

  1. TypeScript 素描-变量声明
  2. debian下NTFS分区无法访问解决
  3. Linux下开发常用 模拟 Http get和post请求
  4. Android -- 闹钟服务的使用(启动与停止)
  5. Redrain个人维护并使用的DuiLib和UiLib库源代码下载地址
  6. HybridApp iOS ATS解决方案
  7. POJ #1141 - Brackets Sequence - TODO: POJ website issue
  8. POJ1338Ugly Numbers(DP)
  9. JS后退, JS返回上一页, JS返回下一页
  10. Python读取Excel数据
  11. js 事件之 createEvent、dispatchEvent
  12. Java基础-工厂设计模式(三锅的肥鸡)
  13. 记我在github上参与的Star增长最快的十万级项目。。。
  14. GoogLeNet 改进之 Inception-v2/v3 解读
  15. 【剑指offer】二进制中1的个数
  16. makefile 必知必会以及Makefile是怎样炼成的
  17. pytest 简介与安装
  18. oozie-ext
  19. swagger and restful api 参考
  20. IDEA 使用generator逆向工程生成pojo,mapper

热门文章

  1. (三) UART 串口通讯
  2. SVN提交时响应很慢,我是这样解决的。
  3. The constructor BASE64Encoder() is not accessible due to restriction on required library
  4. Unity 性能
  5. JAG Summer 2012 Day 4 C Connect
  6. [译]Dynamics AX 2012 R2 BI系列-Cube概览
  7. ubuntu vim8.0源码安装
  8. HTML总结笔记
  9. MySQL字段类型详解
  10. HTML 5 应用程序缓存(上)