Oshocamera。OshoCamera是我独立开发的一款相机应用。应用商店地址:https://itunes.apple.com/cn/app/osho/id1203312279?mt=8。支持1:1、4:3、16:9多种分辨率。可以在取景器中实时预览滤镜。拍摄过程可以实时结合滤镜。支持分段拍摄和回删。先分享一下开发这款APP的一些心得。文末会给出项目的下载地址。阅读本文可能需要一点AVFoundation开发基础。1、GLKView和GPUImageVideoCamera一开始就预览了取景框。我是基于GLKView制作的。GLKView是Apple对OpenGL的封装。我们可以使用它的回调函数-glkView:drawInRect:来渲染处理后的samplebuffer(samplebuffer是由camera回调didOutputSampleBuffer生成的),附上原简化代码:-(CIImage*)renderImageInRect:(CGRect)rect{CMSampleBufferRefsampleBuffer=_sampleBufferHolder.sampleBuffer;if(sampleBuffer!=nil){UIImage*originImage=[selfimageFromSamplePlanerPixelBuffer:sampleBuffer];if(originImage){if(self.filterName&&self.filterName.length>0){GPUImageOutput*filter;if([self.filterTypeisEqual:@"1"]){Classclass=NSClassFromString(self.filterName);filter=[[classalloc]init];}else{NSBundle*bundle=[NSBundlebundleForClass:self.class];NSURL*filterAmaro=[NSURLfileURLWithPath:[bundlepathForResource:self.filterNameofType:@"acv"]];filter=[[GPUImageToneCurveFilteralloc]initWithACVURL:filterAmaro];}[filterforceProcessingAtSize:originImage.size];GPUImagePicture*pic=[[GPUImagePicturealloc]initWithImage:originImage];[picaddTarget:filter];[filteruseNextFrameForImageCapture];[filteraddTarget:self.gpuImageView];[picprocessImage];UIImage*filterImage=[filterimageFromCurrentFramebuffer];//UIImage*filterImage=[filterimageByFilteringImage:originImage];_CIImage=[[CIImagealloc]initWithCGImage:filterImage.CGImageoptions:nil];}else{_CIImage=[CIImageimageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];}}CIImage*image=_CIImage;if(image!=nil){image=[imageimageByApplyingTransform:self].preferredCIImageTransform];if(self.scaleAndResizeCIImageAutomatically){image=[selfscaleAndResizeCIImage:imageforRect:rect];}}returnimage;}-(void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect{@autoreleasepool{rect=CGRectMultiply(rect,self.contentScaleFactor);glClearColor(0,0,0,0);glClear(GL_COLOR_BUFFER_BIT);CIImage*image=[selfrenderImageInRect:rect];if(image!=nil){[_context.CIContextdrawImage:imageinRect:rectfromRect:image.extent];}}}这样的实际上在低端机器上取场景框会明显卡住,ViewController上的列表几乎不能滑动,虽然手势还是可以支持的,因为需要实现分段拍摄,回删等功能。使用这种方式的初衷是期望更高的定制化程度。而不是用GPUImageVideoCamera,毕竟要在AVCaptureVideoDataOutputSampleBufferDelegate和AVCaptureAudioDataOutputSampleBufferDelegate这两个回调上做文章。为了满足需求,只好在不侵入GPUImage源码的情况下努力了。怎样才能不破坏GPUImageVideoCamera的代码呢?我想到了两种方法。首先是创建一个类,然后复制GPUImageVideoCamera中的代码。这很简单粗暴。缺点是以后如果升级GPUImage,代码会一直维护。这是一场小灾难;再来说说第二种方法——继承。继承是一种非常优雅的行为,但它的麻烦在于你无法获取私有变量。幸运的是,有一个强大的运行时可以解决这个难题。下面是使用runtime获取私有变量:-(AVCaptureAudioDataOutput*)gpuAudioOutput{Ivarvar=class_getInstanceVariable([superclass],"audioOutput");idnameVar=object_getIvar(self,var);returnnameVar;}至此取景框已经实现了滤镜的渲染和列表的滑动帧率保证。2.GPUImage的实时合成与outputImageOrientation顾名思义,outputImageOrientation的属性与图像的方向有关。GPUImage的这个属性针对取景器中不同设备的图像方向进行了优化,但是这个优化会和videoOrientation冲突,会导致切换相机时图像方向不正确,也会导致拍摄后视频的方向为是不正确的。最终的解决方案是保证摄像头输出图像的方向是正确的,所以设置为UIInterfaceOrientationPortrait而不是videoOrientation。剩下的问题就是拍摄后视频的方向如何处理。我们先来看视频的实时合成,因为它包含了对用户合成的CVPixelBufferRef资源的处理。或者使用继承来继承GPUImageView,它使用运行时调用私有方法:SELs=NSSelectorFromString(@"textureCoordinatesForRotation:");IMImmp=[[GPUImageViewclass]methodForSelector:s];GLfloat*(*func)(id,SEL,GPUImageRotationMode)=(void*)imp;GLfloat*result=[GPUImageViewclass]?func([GPUImageViewclass],s,inputRotation):nil;......glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute,2,GL_FLOAT,0,0,result);直奔重点——CVPixelBufferRef的处理,将renderTarget转化为CGImageRef对象,然后使用UIGraphics获取方向经过CGAffineTransform处理的UIImage。这时候UIImage的方向就不是正常的方向了,而是图片旋转了90度。这样做的目的是为videoInput的transform属性做铺垫。下面是CVPixelBufferRef的处理代码:intwidth=self.gpuInputFramebufferForDisplay.size.width;intheight=self.gpuInputFramebufferForDisplay.size.height;renderTarget=self.gpuInputFramebufferForDisplay.gpuBufferRef;NSUIntegerpaddedWidthOfImage=CVPixelBufferGetBytesPerRow(renderTarget)/4.0;NSUIntegerpaddedBytesForImage=paddedWidthOfImage*(int)height*4;glFinish();CVPixelBufferLockBaseAddress(renderTarget,0);GLubyte*data=(GLubyte*)CVPixelBufferGetBaseAddress(renderTarget);CGDataProviderRefref=CGDataProviderCreateWithData(NULL,data,paddedBytesForImage,NULL);CGColorSpaceRefcolorspace=CGColorSpaceCreateDeviceRGB();CGImageRefiref();CGImageCreate((int)width,(int)height,8,32,CVPixelBufferGetBytesPerRow(renderTarget),colorspace,kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst,ref,NULL,NO,kCGRenderingIntentDefault);UIGraphicsBeginImageContext(CGSizeMake(高度,宽度));CGGetContextRefcgContexts(CGGetContextRefcgContexts=UIGrent);CGAffineTransformtransform=CGAffineTransformI身份;变换=CGAffineTransformMakeTranslation(高度/2.0,宽度/2.0);变换=CGAffineTransformRotate(变换,M_PI_2);变换=CGAffineTransformScale(变换,1.0,-1.0);CGContextConcatCTM(cgcontext,变换);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=[[CIImagealloc]initWithCGImage:img.CGImageoptions:nil];CVPixelBufferLockBaseAddress(pixelBuffer,0);[self.context.CIContextrender:imagetoCVPixelBuffer:pixelBuffer];...[_videoPixelBufferAdaptorappendPixelBuffer:pixelBufferwithPresentationTime:bufferTimestamp]可以看到关键点还是在上面GPUImageView类继承的renderTarget属性。应该是取景器实时预览的结果。我在初始合成时使用sampleBuffer转换UIImage,然后通过GPUImage添加滤镜,最后将UIImage转换为CIImage,这样会导致拍摄时卡住。当时我差点想放弃,拍完还想通过加滤镜的方式绕过,但最后这些不纯的方法都被我给ban掉了。由于可以在取景器中实时渲染滤镜,我认为GPUImageView可能会有用。看了很多GPUImage的源码,终于在GPUImageFramebuffer.m中找到了一个叫renderTarget的属性。至此,合成的功能也告一段落了。3.关于filter,这里分享一个有趣的过程。应用程序中有三种类型的过滤器。基于glsl,直接使用acv,直接使用lookuptable。Lookuptable其实就是photoshop可以导出的一种图片,但是一般的软件都会加密。我简单说一下我是如何反编译和“借用”某软件的一些滤镜的。使用HopperDisassembler软件进行反编译,然后搜索一些关键词,幸运的在下图中找到了一个方法名。reverse只能说这么多了。。。我已经在开源代码中去掉了这种敏感过滤器。总结开发相机应用程序是一个非常有趣的过程。在里面遇到了很多优秀的开源代码,从开源代码中学习,避免总是写同样的代码。最后附上该项目的开源地址https://github.com/hawk0620/ZPCamera,希望对有需要的朋友有所帮助,欢迎star和pullrequest。