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

WerYWI 8年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/6c5bb92b032c4015fb1b8d60f9cb0c94.jpg"></p>    <h2>1、GLKView和GPUImageVideoCamera</h2>    <p>一开始取景框的预览我是基于 GLKView 做的,GLKView 是苹果对 OpenGL 的封装,我们可以使用它的回调函数 -glkView:drawInRect: 进行对处理后的 samplebuffer 渲染的工作( samplebuffer 是在相机回调 didOutputSampleBuffer 产生的),附上当初简版代码:</p>    <pre>  <code class="language-objectivec">- (CIImage *)renderImageInRect:(CGRect)rect {      CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer;         if (sampleBuffer != nil) {          UIImage *originImage = [selfimageFromSamplePlanerPixelBuffer:sampleBuffer];          if (originImage) {            if (self.filterName && self.filterName.length > 0) {                   GPUImageOutput<GPUImageInput> *filter;                  if ([self.filterTypeisEqual: @"1"]) {                      Class class = NSClassFromString(self.filterName);                      filter = [[class alloc]init];                  } else {                      NSBundle *bundle = [NSBundlebundleForClass:self.class];                      NSURL *filterAmaro = [NSURLfileURLWithPath:[bundlepathForResource:self.filterNameofType:@"acv"]];                      filter = [[GPUImageToneCurveFilter alloc]initWithACVURL:filterAmaro];                  }                  [filterforceProcessingAtSize:originImage.size];                  GPUImagePicture *pic = [[GPUImagePicture alloc]initWithImage:originImage];                  [picaddTarget:filter];                  [filteruseNextFrameForImageCapture];                  [filteraddTarget:self.gpuImageView];                  [picprocessImage];                                UIImage *filterImage = [filterimageFromCurrentFramebuffer];                  //UIImage *filterImage = [filter imageByFilteringImage:originImage];                     _CIImage = [[CIImage alloc]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];          }      }         return image;  }     - (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];          }      }  }     </code></pre>    <p>这样的实现在低端机器上取景框会有明显的卡顿,而且 ViewController 上的列表几乎无法滑动,虽然手势倒是还可以支持。 因为要实现分段拍摄与回删等功能,采用这种方式的初衷是期望更高度的自定义,而不去使用 GPUImageVideoCamera , 毕竟我得在 AVCaptureVideoDataOutputSampleBufferDelegate , AVCaptureAudioDataOutputSampleBufferDelegate 这两个回调做文章,为了满足需求,所以得在不侵入 GPUImage 源代码的前提下点功夫。</p>    <p>怎么样才能在不破坏 GPUImageVideoCamera 的代码呢?我想到两个方法,第一个是创建一个类,然后把 GPUImageVideoCamera 里的代码拷贝过来,这么做简单粗暴,缺点是若以后 GPUImage 升级了,代码维护起来是个小灾难;再来说说第二个方法——继承,继承是个挺优雅的行为,可它的麻烦在于获取不到私有变量,好在有强大的 runtime,解决了这个棘手的问题。下面是用 runtime 获取私有变量:</p>    <pre>  <code class="language-objectivec">- (AVCaptureAudioDataOutput *)gpuAudioOutput {      Ivar var = class_getInstanceVariable([super class], "audioOutput");      id nameVar = object_getIvar(self, var);      return nameVar;  }     </code></pre>    <p>至此取景框实现了滤镜的渲染并保证了列表的滑动帧率。</p>    <h2>2、实时合成以及 GPUImage 的 outputImageOrientation</h2>    <p>顾名思义, outputImageOrientation 属性和图像方向有关的。 GPUImage 的这个属性是对不同设备的在取景框的图像方向做过优化的,但这个优化会与 videoOrientation 产生冲突,它会导致切换摄像头导致图像方向不对,也会造成拍摄完之后的视频方向不对。 最后的解决办法是确保摄像头输出的图像方向正确,所以将其设置为 UIInterfaceOrientationPortrait ,而不对 videoOrientation 进行设置,剩下的问题就是怎样处理拍摄完成之后视频的方向。</p>    <p>先来看看视频的实时合成,因为这里包含了对用户合成的 CVPixelBufferRef 资源处理。还是使用继承的方式继承 GPUImageView ,其中使用了 runtime 调用私有方法:</p>    <pre>  <code class="language-objectivec">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);     </code></pre>    <p>直奔重点—— CVPixelBufferRef 的处理,将 renderTarget 转换为 CGImageRef 对象,再使用 UIGraphics 获得经 CGAffineTransform 处理过方向的 UIImage,此时 UIImage 的方向并不是正常的方向,而是旋转过90度的图片,这么做的目的是为 videoInput 的 transform 属性埋下伏笔。下面是 CVPixelBufferRef 的处理代码:</p>    <pre>  <code class="language-objectivec">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);     </code></pre>    <p>而 videoInput 的 transform 属性设置如下:</p>    <pre>  <code class="language-objectivec">_videoInput.transform = CGAffineTransformRotate(_videoConfiguration.affineTransform, -M_PI_2);     </code></pre>    <p>经过这两次方向的处理,合成的小视频终于方向正常了。此处为简版的合成视频代码:</p>    <pre>  <code class="language-objectivec">CIImage *image = [[CIImage alloc]initWithCGImage:img.CGImageoptions:nil];  CVPixelBufferLockBaseAddress(pixelBuffer, 0);  [self.context.CIContextrender:imagetoCVPixelBuffer:pixelBuffer];  ...  [_videoPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:bufferTimestamp]     </code></pre>    <p>可以看到关键点还是在于上面继承自 GPUImageView 这个类获取到的 renderTarget 属性,它应该即是取景框实时预览的结果,我在最初的合成中是使用 sampleBuffer 转 UIImage,再通过 GPUImage 添加滤镜,最后将 UIImage 再转 CIImage,这么做导致拍摄时会卡。当时我几乎想放弃了,甚至想采用拍好后再加滤镜的方式绕过去,最后这些不纯粹的方法都被我 ban 掉了。</p>    <p>既然滤镜可以在取景框实时渲染,我想到了 GPUImageView 可能有料。在阅读过 GPUImage 的诸多源码后,终于在 GPUImageFramebuffer.m 找到了一个叫 renderTarget 的属性。至此,合成的功能也告一段落。</p>    <h2>3、关于滤镜</h2>    <p>这里主要分享个有意思的过程。App 里有三种类型的滤镜。基于 glsl 的、直接使用 acv 的以及直接使用 lookuptable 的。lookuptable 其实也是 photoshop 可导出的一种图片,但一般的软件都会对其加密,下面简单提下我是如何反编译“借用”某软件的部分滤镜吧。使用 Hopper Disassembler 软件进行反编译,然后通过某些关键字的搜索,幸运地找到了下图的一个方法名。</p>    <p><img src="https://simg.open-open.com/show/e156b3a562345ca1e45e5f7df6636987.jpg"></p>    <p>reverse 只能说这么多了….在开源代码里我已将这一类敏感的滤镜剔除了。</p>    <h2>小结</h2>    <p>开发相机 App 是个挺有意思的过程,在其中邂逅不少优秀开源代码,向开源代码学习,才能避免自己总是写出一成不变的代码。最</p>    <p> </p>    <p> </p>    <p> </p>