开源一个上架App Store的相机App

Osho 相机是我独立开发上架的一个相机 App。它支持1:1,4:3,16:9多种分辨率拍摄,滤镜可在取景框的实时预览,拍摄过程可与滤镜实时合成,支持分段拍摄,支持回删等特性。下面先分享分享开发这个 App 的一些心得体会,文末会给出项目的下载地址,阅读本文可能需要一点点 AVFoundation 开发的基础。 

1、GLKView和GPUImageVideoCamera

一开始取景框的预览我是基于 GLKView 做的,GLKView 是苹果对 OpenGL 的封装,我们可以使用它的回调函数 -glkView:drawInRect: 进行对处理后的 samplebuffer 渲染的工作(samplebuffer 是在相机回调 didOutputSampleBuffer 产生的),附上当初简版代码:


  1. - (CIImage *)renderImageInRect:(CGRect)rect { 
  2.  
  3.     CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer; 
  4.  
  5.   
  6.  
  7.     if (sampleBuffer != nil) { 
  8.  
  9.         UIImage *originImage = [self imageFromSamplePlanerPixelBuffer:sampleBuffer]; 
  10.  
  11.         if (originImage) { 
  12.  
  13.            if (self.filterName && self.filterName.length > 0) { 
  14.  
  15.   
  16.  
  17.                GPUImageOutput<GPUImageInput> *filter; 
  18.  
  19.                 if ([self.filterType isEqual: @"1"]) { 
  20.  
  21.                     Class class = NSClassFromString(self.filterName); 
  22.  
  23.                     filter = [[class alloc] init]; 
  24.  
  25.                 } else { 
  26.  
  27.                     NSBundle *bundle = [NSBundle bundleForClass:self.class]; 
  28.  
  29.                     NSURL *filterAmaro = [NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]]; 
  30.  
  31.                     filter = [[GPUImageToneCurveFilter alloc] initWithACVURL:filterAmaro]; 
  32.  
  33.                 } 
  34.  
  35.                 [filter forceProcessingAtSize:originImage.size]; 
  36.  
  37.                 GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:originImage]; 
  38.  
  39.                 [pic addTarget:filter]; 
  40.  
  41.                 [filter useNextFrameForImageCapture]; 
  42.  
  43.                 [filter addTarget:self.gpuImageView]; 
  44.  
  45.                 [pic processImage];               
  46.  
  47.                 UIImage *filterImage = [filter imageFromCurrentFramebuffer]; 
  48.  
  49.                 //UIImage *filterImage = [filter imageByFilteringImage:originImage]; 
  50.  
  51.   
  52.  
  53.                 _CIImage = [[CIImage alloc] initWithCGImage:filterImage.CGImage options:nil]; 
  54.  
  55.             } else { 
  56.  
  57.             _CIImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)]; 
  58.  
  59.         } 
  60.  
  61.     }   
  62.  
  63.     CIImage *image = _CIImage; 
  64.  
  65.   
  66.  
  67.     if (image != nil) { 
  68.  
  69.         image = [image imageByApplyingTransform:self.preferredCIImageTransform]; 
  70.  
  71.   
  72.  
  73.         if (self.scaleAndResizeCIImageAutomatically) { 
  74.  
  75.            image = [self scaleAndResizeCIImage:image forRect:rect]; 
  76.  
  77.         } 
  78.  
  79.     } 
  80.  
  81.   
  82.  
  83.     return image; 
  84.  
  85.  
  86.   
  87.  
  88. - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { 
  89.  
  90.     @autoreleasepool { 
  91.  
  92.         rect = CGRectMultiply(rect, self.contentScaleFactor); 
  93.  
  94.         glClearColor(0, 0, 0, 0); 
  95.  
  96.         glClear(GL_COLOR_BUFFER_BIT); 
  97.  
  98.   
  99.  
  100.         CIImage *image = [self renderImageInRect:rect]; 
  101.  
  102.   
  103.  
  104.         if (image != nil) { 
  105.  
  106.             [_context.CIContext drawImage:image inRect:rect fromRect:image.extent]; 
  107.  
  108.         } 
  109.  
  110.     } 
  111.  
  112. }  

这样的实现在低端机器上取景框会有明显的卡顿,而且 ViewController 上的列表几乎无法滑动,虽然手势倒是还可以支持。 因为要实现分段拍摄与回删等功能,采用这种方式的初衷是期望更高度的自定义,而不去使用 GPUImageVideoCamera, 毕竟我得在 AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate 这两个回调做文章,为了满足需求,所以得在不侵入 GPUImage 源代码的前提下点功夫。

怎么样才能在不破坏 GPUImageVideoCamera 的代码呢?我想到两个方法,第一个是创建一个类,然后把 GPUImageVideoCamera 里的代码拷贝过来,这么做简单粗暴,缺点是若以后 GPUImage 升级了,代码维护起来是个小灾难;再来说说第二个方法——继承,继承是个挺优雅的行为,可它的麻烦在于获取不到私有变量,好在有强大的 runtime,解决了这个棘手的问题。下面是用 runtime 获取私有变量:


  1. - (AVCaptureAudioDataOutput *)gpuAudioOutput { 
  2.  
  3.     Ivar var = class_getInstanceVariable([super class], "audioOutput"); 
  4.  
  5.     id nameVar = object_getIvar(self, var); 
  6.  
  7.     return nameVar; 
  8.  
  9. }  

至此取景框实现了滤镜的渲染并保证了列表的滑动帧率。

2、实时合成以及 GPUImage 的 outputImageOrientation

顾名思义,outputImageOrientation 属性和图像方向有关的。GPUImage 的这个属性是对不同设备的在取景框的图像方向做过优化的,但这个优化会与 videoOrientation 产生冲突,它会导致切换摄像头导致图像方向不对,也会造成拍摄完之后的视频方向不对。 最后的解决办法是确保摄像头输出的图像方向正确,所以将其设置为 UIInterfaceOrientationPortrait,而不对 videoOrientation 进行设置,剩下的问题就是怎样处理拍摄完成之后视频的方向。

先来看看视频的实时合成,因为这里包含了对用户合成的 CVPixelBufferRef 资源处理。还是使用继承的方式继承 GPUImageView,其中使用了 runtime 调用私有方法:


  1. SEL s = NSSelectorFromString(@"textureCoordinatesForRotation:"); 
  2.  
  3. IMP imp = [[GPUImageView class] methodForSelector:s]; 
  4.  
  5. GLfloat *(*func)(id, SEL, GPUImageRotationMode) = (void *)imp; 
  6.  
  7. GLfloat *result = [GPUImageView class] ? func([GPUImageView class], s, inputRotation) : nil; 
  8.  
  9.   
  10.  
  11. ...... 
  12.  
  13.   
  14.  
  15. glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, result);  

直奔重点——CVPixelBufferRef 的处理,将 renderTarget 转换为 CGImageRef 对象,再使用 UIGraphics 获得经 CGAffineTransform 处理过方向的 UIImage,此时 UIImage 的方向并不是正常的方向,而是旋转过90度的图片,这么做的目的是为 videoInput 的 transform 属性埋下伏笔。下面是 CVPixelBufferRef 的处理代码:


  1. int width = self.gpuInputFramebufferForDisplay.size.width; 
  2.  
  3. int height = self.gpuInputFramebufferForDisplay.size.height; 
  4.  
  5.   
  6.  
  7. renderTarget = self.gpuInputFramebufferForDisplay.gpuBufferRef; 
  8.  
  9.   
  10.  
  11. NSUInteger paddedWidthOfImage = CVPixelBufferGetBytesPerRow(renderTarget) / 4.0; 
  12.  
  13. NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)height * 4; 
  14.  
  15.   
  16.  
  17. glFinish(); 
  18.  
  19. CVPixelBufferLockBaseAddress(renderTarget, 0); 
  20.  
  21. GLubyte *data = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget); 
  22.  
  23. CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, paddedBytesForImage, NULL); 
  24.  
  25. CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); 
  26.  
  27. CGImageRef iref = CGImageCreate((int)width, (int)height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, ref, NULL, NO, kCGRenderingIntentDefault); 
  28.  
  29.   
  30.  
  31. UIGraphicsBeginImageContext(CGSizeMake(height, width)); 
  32.  
  33. CGContextRef cgcontext = UIGraphicsGetCurrentContext(); 
  34.  
  35. CGAffineTransform transform = CGAffineTransformIdentity; 
  36.  
  37. transform = CGAffineTransformMakeTranslation(height / 2.0, width / 2.0); 
  38.  
  39. transform = CGAffineTransformRotate(transform, M_PI_2); 
  40.  
  41. transform = CGAffineTransformScale(transform, 1.0, -1.0); 
  42.  
  43. CGContextConcatCTM(cgcontext, transform); 
  44.  
  45.   
  46.  
  47. CGContextSetBlendMode(cgcontext, kCGBlendModeCopy); 
  48.  
  49. CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref); 
  50.  
  51. UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 
  52.  
  53. UIGraphicsEndImageContext(); 
  54.  
  55. self.img = image; 
  56.  
  57.   
  58.  
  59. CFRelease(ref); 
  60.  
  61. CFRelease(colorspace); 
  62.  
  63. CGImageRelease(iref); 
  64.  
  65. CVPixelBufferUnlockBaseAddress(renderTarget, 0); 

而 videoInput 的 transform 属性设置如下:


  1. _videoInput.transform = CGAffineTransformRotate(_videoConfiguration.affineTransform, -M_PI_2); 

经过这两次方向的处理,合成的小视频终于方向正常了。此处为简版的合成视频代码:


  1. CIImage *image = [[CIImage alloc] initWithCGImage:img.CGImage options:nil]; 
  2.  
  3. CVPixelBufferLockBaseAddress(pixelBuffer, 0); 
  4.  
  5. [self.context.CIContext render:image toCVPixelBuffer:pixelBuffer]; 
  6.  
  7. ... 
  8.  
  9. [_videoPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:bufferTimestamp]  

可以看到关键点还是在于上面继承自 GPUImageView 这个类获取到的 renderTarget 属性,它应该即是取景框实时预览的结果,我在最初的合成中是使用 sampleBuffer 转 UIImage,再通过 GPUImage 添加滤镜,最后将 UIImage 再转 CIImage,这么做导致拍摄时会卡。当时我几乎想放弃了,甚至想采用拍好后再加滤镜的方式绕过去,最后这些不纯粹的方法都被我 ban 掉了。

既然滤镜可以在取景框实时渲染,我想到了 GPUImageView 可能有料。在阅读过 GPUImage 的诸多源码后,终于在 GPUImageFramebuffer.m 找到了一个叫 renderTarget 的属性。至此,合成的功能也告一段落。

3、关于滤镜

这里主要分享个有意思的过程。App 里有三种类型的滤镜。基于 glsl 的、直接使用 acv 的以及直接使用 lookuptable 的。lookuptable 其实也是 photoshop 可导出的一种图片,但一般的软件都会对其加密,下面简单提下我是如何反编译“借用”某软件的部分滤镜吧。使用 Hopper Disassembler 软件进行反编译,然后通过某些关键字的搜索,幸运地找到了下图的一个方法名。

reverse 只能说这么多了….在开源代码里我已将这一类敏感的滤镜剔除了。

小结

开发相机 App 是个挺有意思的过程,在其中邂逅不少优秀开源代码,向开源代码学习,才能避免自己总是写出一成不变的代码。最后附上项目的开源地址 https://github.com/hawk0620/ZPCamera,希望能够帮到有需要的朋友,也欢迎 star 和 pull request。

作者:伯乐专栏/陈浩

来源:51CTO

时间: 2024-10-07 18:57:15

开源一个上架App Store的相机App的相关文章

iphone怎么隐藏app store,ipad隐藏app store方法

1.在我们的iphone或ipad桌面点击[设置]应用中,找到[通用]选项,之后点击打开进入. 2.然后在进入到的界面中点击[访问限制]选项,并输入访问限制的密码.(如果没有创建过密码需要输入新密码创建一个访问密码) 4.好了进入之后点击[安装应用]选项,关闭该功能即可隐藏App Store应用. 5.隐藏效果,当然,隐藏了如果要使用我们可以使用iphone/ipad搜索中输入App Store,即可显示App Store应用应用.

2017最新App Store 审核指南中文版

简介 App 正在改变世界,丰富人们的生活,并为像您一样的开发者提供前所未有的创新机会.因此,App Store 已成长为一个激动人心且充满活力的生态系统,正为数百万的开发者和超过十亿的用户提供服务.不管是开发新手,还是由经验丰富的程序员所组成的大型团队,我们都非常欢迎您为 App Store 创建 app,并希望能够帮助您了解我们的准则,以确保您的 app 能够快速通过审核流程. App Store 的指导原则非常简单:我们希望为用户获取 app 时提供更安全可靠的体验,并为所有开发者提供借助

iOS开发者必备:App Store营销广告指南

很重要:需要征得苹果同意 所有印制和视频格式的材料,无论是营销还是广告,都必须在发表或发布之前提交给苹果并通过苹果的审核.请按照以下指导通过电子邮件提交你的构想.情节串联图板或者最终的创意.回复至少需要5个工作日.苹果可以检查尚未完成的材料:然而,在发布之前所有材料都必须经过苹果的审核. 所有包含苹果产品的定制影像或视频材料在发布或发表以前都必须提交给苹果,并通过苹果的审核. 营销审核:提交给苹果进行审核的营销材料应该发送至appstoremarketing@apple.com.营销包括了任何不

教你在浏览器中快速搜索App Store应用

  之前有老外发现将Mac App Store和iTunes Store这两款软件中的应用图标拖入浏览器可以直接访问其URL,现在又发现一个小巧门,在地址栏输入"应用名称+ site+冒号+itunes.apple.com",即可在Google检索出该应用在App Store和Mac App Store中所有的URL,比如: 在地址栏搜索:QQ site:itunes.apple.com 那么就会罗列出QQ在各平台上的App URL 或者通过在itunes.apple.com后加上/互

苹果App Store公布2013年度iPad App榜单

苹果App Store公布2013年度精选iPad应用游戏_数码_腾讯网腾讯数码讯(徐萧梓丞)既然苹果App Store每到年末都会为大家推荐年度精选应用,那么今年同样也不例外.之前我们已经为大家总结了2013年年度精选iPhone应用和游戏,那么接下来相比一定就该轮到iPad专用的游戏和应用了.作为拥有更大屏幕和更高操作性的平板电脑,在游戏和应用上iPad与iPhone都有所区别.iPhone的精选应用更注重实用性,而iPad的精选更强调欣赏和操作.下面让我们一起来看看究竟有哪行App入选了年

旅游类App如何设置关键词在app store获取更多下载量

用户通过iTunes,app store平台下载app应用的途径 1.通过总榜或分榜应用排名来获取应用; 2.通过关键词搜索来获取应用; 通过下面的分析得出旅行类的app搜索的关键词主要为:旅游,旅行,旅游攻略,旅行者,旅行家,自助游,旅游指南,旅游大全,旅途必备,景点大全,出行工具,旅行工具,旅行计划,附件餐馆美食.景点,照片分享,游记大多数应用标题命名规则:旅行攻略+游记,网站品牌名+旅游/旅行/攻略/景点/景点门票/机票/酒店/特价机票/火车票/旅游指南/旅游大全/景点大全. 关键词排名跟

怎么在iOS和OS X的iTunes App Store中隐藏购买项目

  对于iOS平台的App Store,你只需要进入已购买列表,然后在应用边上像删除短信一样,滑动手指,红色的隐藏按钮就会出现,点击之后就会被从购买记录中隐藏. 对于iTunes App Store或者Mac App Store. 首先点击已购买,然后右键点击选择隐藏购买项.当然了, 藏起来的东西要在想找的时候找得到才有意义.那么下面我们来看看如何解除隐藏.对于iOS,则进入iTunes&iCloud账号设置,找到隐藏的购买项,进入后就能够看到已经隐藏的项目. 对于OS X来说,点击Mac Ap

是我的账号不对么-App Store的提交问题 PLA 1.2

问题描述 App Store的提交问题 PLA 1.2 PLA 1.2 We found that the Seller and/or Artist names associated with your app do not reflect the name, "深圳大桔互联网金融服务有限公司," in the app and/or its name and metadata. To be appropriate for the App Store, your app must be

春雨计步器上榜app store,主打“运动社交”

上周,刚刚融资完成的春雨医生推出新产品"春雨计步器",新产品提供计步功能的同时,主打"运动社交".上线不到一周,成绩斐然,更于本周五登上了app store优秀新app的榜首. 虽然市场上计步软件众多,春雨计步器能从中http://www.aliyun.com/zixun/aggregation/7814.html">脱颖而出的主要原因是引入了"社交元素".其中的"好友约战"功能,能够让用户邀请微信好友,比拼每