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. CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer;
  3. if (sampleBuffer != nil) {
  4. UIImage *originImage = [self imageFromSamplePlanerPixelBuffer:sampleBuffer];
  5. if (originImage) {
  6. if (self.filterName && self.filterName.length > 0) {
  7. GPUImageOutput<GPUImageInput> *filter;
  8. if ([self.filterType isEqual: @"1"]) {
  9. Class class = NSClassFromString(self.filterName);
  10. filter = [[class alloc] init];
  11. } else {
  12. NSBundle *bundle = [NSBundle bundleForClass:self.class];
  13. NSURL *filterAmaro = [NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]];
  14. filter = [[GPUImageToneCurveFilter alloc] initWithACVURL:filterAmaro];
  15. }
  16. [filter forceProcessingAtSize:originImage.size];
  17. GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:originImage];
  18. [pic addTarget:filter];
  19. [filter useNextFrameForImageCapture];
  20. [filter addTarget:self.gpuImageView];
  21. [pic processImage];
  22. UIImage *filterImage = [filter imageFromCurrentFramebuffer];
  23. //UIImage *filterImage = [filter imageByFilteringImage:originImage];
  24. _CIImage = [[CIImage alloc] initWithCGImage:filterImage.CGImage options:nil];
  25. } else {
  26. _CIImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
  27. }
  28. }
  29. CIImage *image = _CIImage;
  30. if (image != nil) {
  31. image = [image imageByApplyingTransform:self.preferredCIImageTransform];
  32. if (self.scaleAndResizeCIImageAutomatically) {
  33. image = [self scaleAndResizeCIImage:image forRect:rect];
  34. }
  35. }
  36. return image;
  37. }
  38. - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
  39. @autoreleasepool {
  40. rect = CGRectMultiply(rect, self.contentScaleFactor);
  41. glClearColor(0, 0, 0, 0);
  42. glClear(GL_COLOR_BUFFER_BIT);
  43. CIImage *image = [self renderImageInRect:rect];
  44. if (image != nil) {
  45. [_context.CIContext drawImage:image inRect:rect fromRect:image.extent];
  46. }
  47. }
  48. }

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

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

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

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

2、实时合成以及 GPUImage 的 outputImageOrientation

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

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

  1. SEL s = NSSelectorFromString(@"textureCoordinatesForRotation:");
  2. IMP imp = [[GPUImageView class] methodForSelector:s];
  3. GLfloat *(*func)(id, SEL, GPUImageRotationMode) = (void *)imp;
  4. GLfloat *result = [GPUImageView class] ? func([GPUImageView class], s, inputRotation) : nil;
  5. ......
  6. 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. int height = self.gpuInputFramebufferForDisplay.size.height;
  3. renderTarget = self.gpuInputFramebufferForDisplay.gpuBufferRef;
  4. NSUInteger paddedWidthOfImage = CVPixelBufferGetBytesPerRow(renderTarget) / 4.0;
  5. NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)height * 4;
  6. glFinish();
  7. CVPixelBufferLockBaseAddress(renderTarget, 0);
  8. GLubyte *data = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget);
  9. CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, paddedBytesForImage, NULL);
  10. CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
  11. CGImageRef iref = CGImageCreate((int)width, (int)height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, ref, NULL, NO, kCGRenderingIntentDefault);
  12. UIGraphicsBeginImageContext(CGSizeMake(height, width));
  13. CGContextRef cgcontext = UIGraphicsGetCurrentContext();
  14. CGAffineTransform transform = CGAffineTransformIdentity;
  15. transform = CGAffineTransformMakeTranslation(height / 2.0, width / 2.0);
  16. transform = CGAffineTransformRotate(transform, M_PI_2);
  17. transform = CGAffineTransformScale(transform, 1.0, -1.0);
  18. CGContextConcatCTM(cgcontext, transform);
  19. CGContextSetBlendMode(cgcontext, kCGBlendModeCopy);
  20. CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref);
  21. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  22. UIGraphicsEndImageContext();
  23. self.img = image;
  24. CFRelease(ref);
  25. CFRelease(colorspace);
  26. CGImageRelease(iref);
  27. 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. CVPixelBufferLockBaseAddress(pixelBuffer, 0);
  3. [self.context.CIContext render:image toCVPixelBuffer:pixelBuffer];
  4. ...
  5. [_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

开源一个上架App Store的相机App相关推荐

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

    原创 2017-02-21 伯乐专栏/陈浩 iOS大全 (点击上方公众号,可快速关注) 来源:伯乐在线 - Hawk0620 如有好文章投稿,请点击 → 这里了解详情 如需转载,发送「转载」二字查看说 ...

  2. App Store 上传app后不能构建版本,构建版本发现不了已上传app , 没有➕号 一定要查看App Store账号邮箱

    1.首先要看用什么工具上传的 第一次往App Store上传app最好用Application Loader 不要用xcode直接上传因为 xcode直接上传如果app当中有问题不会报错,比如icon ...

  3. 解决 Invalid App Store Icon - The App Store Icon in the asset catalog in ‘HBuilder.app‘ can‘t

    App Store Connect Dear Developer, We identified one or more issues with a recent delivery for your a ...

  4. ERROR ITMS-90717: “Invalid App Store Icon. The App Store Icon in the asset catalog in ‘HBuilder.app‘

    前言 注意:web App 应用,原生开发可作为参考. 上传应用到 App Store 时,交付后出现了如下问题: #SEO ERROR ITMS-90717: "Invalid App S ...

  5. App Store Connect显示app已经上架(可供销售),但在App Store中没有实时更新

    记得去年(2016年)的时候,上线的时候,基本上审核通过的时间平均是3天左右,有时候在你很急的时候,甚至会让你等到一周左右:但是最近发现上线很快,最近(2017年11月)提交,即使是晚上22:00左右 ...

  6. 从App Store获取最新APP版本号

    CFBundleShortVersionString(Version) :真正的APP版本号,即应用程序的迭代版本: APP Store 所识别到的版本号.发布版本号是三个时期分离整数组成的字符串.例 ...

  7. App Store上推广App的实战经验

    分享到:新浪微博 如今周围层出不穷的移动创富故事,很容易让人心血沸腾,似乎自己也能创造出下一个<愤怒的小鸟>,空中网洪亮说自己在09年的时候也这样想过,并且还实际组队做个几个产品,结果销量 ...

  8. app store服务器网站,app store 游戏服务器

    app store 游戏服务器 内容精选 换一换 可以.您可以在云服务器关机后,对云服务器变更规格.温馨提醒:目前包周期云服务器只支持升级配置. 可以按照玩家数量.游戏复杂度.游戏分区分服架构来选购服 ...

  9. 微信里扫描二维码弹出默认浏览器(苹果打开App Store)打开app的下载链接怎么实现

    使用微信推广的用户经常都会遇到推广链接被拦截导致无法下载app的情况,此时用户在微信中打开会提示" 已停止访问该网页 ".这对于使用微信营销的商家来说就很不友好且损失非常大,因为用 ...

最新文章

  1. 好文 | “智能经济”时代,协作机器人的应用前景与趋势探讨
  2. 足不出户完成交付独家交付秘籍(第二回)
  3. java操作samba_使用Java和Samba JCIFS访问文件
  4. centos7 iptables 端口转发 保存_iptables 防火墙
  5. C++结构体作为函数参数传参
  6. 【Flink】Flink 的状态描述符 StateDescriptor operator state key state
  7. se linux ll-z,Linux selinux 基础
  8. 【android自定义控件】属性动画 五
  9. php ddos 防御,PHP DDos的几个防御方法详解
  10. html复制到word乱码,word文档打开遇到错误 为什么复制粘贴后乱码
  11. 计算机网络练习题-3
  12. 数字电视音视频马赛克和不同步现象原因
  13. 0x80073712_处理win10更新提示错误代码“0x80073712”的方法
  14. 【Django】Python+Django 图文教程
  15. (NO.00001)iOS游戏SpeedBoy Lite成形记(十九)
  16. 北漂码农的真实心声:赚一线城市的钱,还二线城市的房贷
  17. 2016年8月1日 星期一 --出埃及记 Exodus 16:1
  18. 《软件功能测试自动化实战教程》—第6章6.5节使用环境变量的参数化
  19. 从unpkg上下载资源
  20. wordpress配置SMTP服务发送邮件(qq邮箱)

热门文章

  1. SOA之(2)——SOA架构基础概念与设计框架
  2. 如何在CentOS6.2上安装并运行飞鸽传书
  3. Squid部署文档一
  4. ASP.Net网站开发的单元测试方案
  5. Forbid consumer 192.168.85.1 access service com.sharearn.dubbo.romote.TestService from registry
  6. Linux中的动态库和静态库(.a/.la/.so/.o)
  7. Asp.net MVC生命周期
  8. /var/spool/clientmqueue文件分析
  9. 大比拼:用24种可视化工具完成同一项任务的心得体会
  10. js判断是否包含指定字符串