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

1、GLKView和GPUImageVideoCamera

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

- (CIImage *)renderImageInRect:(CGRect)rect {

CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer;

if (sampleBuffer != nil) {

UIImage *originImage = [self imageFromSamplePlanerPixelBuffer:sampleBuffer];

if (originImage) {

if (self.filterName && self.filterName.length > 0) {

GPUImageOutput<GPUImageInput> *filter;

if ([self.filterType isEqual: @"1"]) {

Class class = NSClassFromString(self.filterName);

filter = [[class alloc] init];

} else {

NSBundle *bundle = [NSBundle bundleForClass:self.class];

NSURL *filterAmaro = [NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]];

filter = [[GPUImageToneCurveFilter alloc] initWithACVURL:filterAmaro];

}

[filter forceProcessingAtSize:originImage.size];

GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:originImage];

[pic addTarget:filter];

[filter useNextFrameForImageCapture];

[filter addTarget:self.gpuImageView];

[pic processImage];

UIImage *filterImage = [filter imageFromCurrentFramebuffer];

//UIImage *filterImage = [filter imageByFilteringImage:originImage];

_CIImage = [[CIImage alloc] initWithCGImage:filterImage.CGImage options:nil];

} else {

_CIImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];

}

}

CIImage *image = _CIImage;

if (image != nil) {

image = [image imageByApplyingTransform:self.preferredCIImageTransform];

if (self.scaleAndResizeCIImageAutomatically) {

image = [self scaleAndResizeCIImage:image forRect:rect];

}

}

return image;

}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {

@autoreleasepool {

rect = CGRectMultiply(rect, self.contentScaleFactor);

glClearColor(0, 0, 0, 0);

glClear(GL_COLOR_BUFFER_BIT);

CIImage *image = [self renderImageInRect:rect];

if (image != nil) {

[_context.CIContext drawImage:image inRect:rect fromRect:image.extent];

}

}

}

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

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

- (AVCaptureAudioDataOutput *)gpuAudioOutput {

Ivar var = class_getInstanceVariable([super class], "audioOutput");

id nameVar = object_getIvar(self, var);

return nameVar;

}

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

2、实时合成以及 GPUImage 的 outputImageOrientation

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

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

SEL s = NSSelectorFromString(@"textureCoordinatesForRotation:");

IMP imp = [[GPUImageView class] methodForSelector:s];

GLfloat *(*func)(id, SEL, GPUImageRotationMode) = (void *)imp;

GLfloat *result = [GPUImageView class] ? func([GPUImageView class], s, inputRotation) : nil;

......

glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, result);

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

int width = self.gpuInputFramebufferForDisplay.size.width;

int height = self.gpuInputFramebufferForDisplay.size.height;

renderTarget = self.gpuInputFramebufferForDisplay.gpuBufferRef;

NSUInteger paddedWidthOfImage = CVPixelBufferGetBytesPerRow(renderTarget) / 4.0;

NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)height * 4;

glFinish();

CVPixelBufferLockBaseAddress(renderTarget, 0);

GLubyte *data = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget);

CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, paddedBytesForImage, NULL);

CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();

CGImageRef iref = CGImageCreate((int)width, (int)height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget),colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, ref, NULL, NO,kCGRenderingIntentDefault);

UIGraphicsBeginImageContext(CGSizeMake(height, width));

CGContextRef cgcontext = UIGraphicsGetCurrentContext();

CGAffineTransform transform = CGAffineTransformIdentity;

transform = CGAffineTransformMakeTranslation(height / 2.0, width / 2.0);

transform = CGAffineTransformRotate(transform, M_PI_2);

transform = CGAffineTransformScale(transform, 1.0, -1.0);

CGContextConcatCTM(cgcontext, transform);

CGContextSetBlendMode(cgcontext, kCGBlendModeCopy);

CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

self.img = image;

CFRelease(ref);

CFRelease(colorspace);

CGImageRelease(iref);

CVPixelBufferUnlockBaseAddress(renderTarget, 0);

而 videoInput 的 transform 属性设置如下:

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

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

CIImage *image = [[CIImage alloc] initWithCGImage:img.CGImage options:nil];

CVPixelBufferLockBaseAddress(pixelBuffer, 0);

[self.context.CIContext render:image toCVPixelBuffer:pixelBuffer];

...

[_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。

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

  1. Invalid App Store Icon. The App Store Icon in the asset catalog in 'xxx.app' can’t be transparent nor contain an alpha channel.

    1.向appstore上传应用的时候,报了这样一个错误 ERROR ITMS-90717: "Invalid App Store Icon. The App Store Icon in th ...

  2. [App Store Connect帮助]四、添加 App 图标、App 预览和屏幕快照(1)App Store 图标、App 预览和屏幕快照概述

    您可以为您的 App Store 产品页提供有关您 App 的 App Store 图标.三个 App 预览和十张屏幕快照. App Store 图标 您必须提供一个 App Store 图标,用于在 ...

  3. archive后upload to app store时遇到app id不可用的问题

    问题如下图 出现此问题的原因有两种: 1.此app id在AppStore中已经存在,也就是说你使用别人注册的app ID ,  如果是这样,你只能更换app ID 2.此app ID是自己的,突然之 ...

  4. WP8__从windowsphone app store 中根据app id获取应用的相关信息(下载网址及图片id等)

    windows phone 官网应用商店地址 http://www.windowsphone.com/zh-cn/store/featured-apps------------------------ ...

  5. APP Store上架QA&注意事项

    一. App Store上架费用,要多少钱. 这个因产品而异,一般是6000-10000元人民币. 二. App Store上架周期,要多久过. 这个因产品而异,正常的话一周内,如果产品老是出问题,被 ...

  6. IOS 上架到App Store被拒的常见问题总结

    Guideline 2.3.3 - Performance - Accurate Metadata 2017年11月16日 上午12:52 发件人 Apple 2. 3 Performance: Ac ...

  7. [苹果APP上架]ios App Store上架详细教程-一条龙顺滑上架-适合小白

    如何在 2022 年将您的应用提交到 App Store 您正在启动您的第一个应用程序,或者距离上次已经有一段时间了.作者纸飞机@cheng716051来给你讲讲将应用程序提交到 App Store ...

  8. App Store审核被拒的23个理由

    原文地址 iOS 应用提交审核要持续一周或者更久,在提交之前,我们一定要进行「自我审查」,避免被拒.ASO100 为大家收集整理了2015年 App Store 审核被拒的23个理由,并且附上官方拒绝 ...

  9. App 被拒 -- App Store Review Guidelines (2015)中英文对照

    Introduction(简介) We're pleased that you want to invest your talents and time to develop applications ...

随机推荐

  1. Centos 6.9 安装 Redis 3.2.9

    依赖包和常用包yum install gcc gcc-c++ make zlib-devel readline readline-devel tkutil tk tkutil-devel tk-dev ...

  2. Postman+newman+jenkins构建

      最近忙着项目接口测试,经过不同工具的对比,发现postman使用起来挺顺手的,所以马上决定使用这个工具进行接口测试工作.刚开始的时候,了解了下接口测试的相关信息,直接着手编写接口测试的测试用例信息 ...

  3. Python网络编程篇之select和epoll

    1. select 原理 在多路复⽤的模型中, ⽐较常⽤的有select模型和epoll模型. 这两个都是系统接⼝, 由操作系统提供. 当然, Python的select模块进⾏了更⾼级的封装. ⽹络 ...

  4. [线段树]P1047 校门外的树

    题目描述 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米.我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置:数轴上的每个整数点,即0,1,2,……,L,都种 ...

  5. BST 解析 (二)height and deletion

    前面一章介绍了BST的结构和一些简单的基本功能,例如:insert,findMin,nextLarger等等.这一节主要讲解一些BST的delete node操作还有BST的height的分析以及一些 ...

  6. SQL基础知识一

    一.建库 show databases ;//显示已存在的数据库 create database 数据库库名; use 数据库名;//进入新建的数据 二.建表 show tables;//查询已存在的 ...

  7. (四):C++分布式框架——状态中心模块

    (四):C++分布式框架--状态中心模块 上篇:(三):C++分布式实时应用框架--系统管理模块 技术交流合作QQ群:436466587 欢迎讨论交流 版权声明:本文版权及所用技术归属smartguy ...

  8. Tomcat 日志分割

    一.前言 随着每天业务的增长,Tomcat 的catalina.out日志 变得越来越大,占用磁盘空间不说.要查看某个时候的日志的时候,庞大的日志让你顿时无从下手,所以日志的切割的变得刻不容缓.而且, ...

  9. Sublime text3 代码格式化插件

    使用 Sublime text 3 格式化HTML代码,需要安装插件,具体安装步骤如下: 1. 打开菜单 -> 首选项(Perferences) -> 插件控制(Packpage Cont ...

  10. 封装及propery的使用

    封装 封装的目的 使类中的属性或者方法只允许在类内部使用,不允许外部对其访问,保证数据的安全性. 封装的方法 使属性或者函数名改写成:"__属性名或者函数名"的格式,即完成了对本类 ...