1. 程式人生 > >開源一個上架 App Store 的相機 App

開源一個上架 App Store 的相機 App

1、GLKView和GPUImageVideoCamera

一開始取景框的預覽我是基於 GLKView 做的,GLKView 是蘋果對 OpenGL 的封裝,我們可以使用它的回撥函式 -glkView:drawInRect: 進行對處理後的 samplebuffer 渲染的工作(samplebuffer 是在相機回撥 didOutputSampleBuffer 產生的),附上當初簡版程式碼:

Objective-C
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 -(CIImage*)renderImageInRect:(CGRect)rect{CMSampleBufferRefsampleBuffer=_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"]){Classclass=NSClassFromString(self.filterName);filter=[[classalloc] init];}else{NSBundle*bundle=[NSBundle bundleForClass
:self.class];NSURL*filterAmaro=[NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]];filter=[[GPUImageToneCurveFilteralloc] initWithACVURL:filterAmaro];}[filter forceProcessingAtSize:originImage.size];GPUImagePicture*pic=[[GPUImagePicturealloc] initWithImage:originImage];[pic addTarget:filter];[filter useNextFrameForImageCapture];[filter addTarget:self.gpuImageView];[pic processImage];UIImage*filterImage=[filter imageFromCurrentFramebuffer];//UIImage *filterImage = [filter imageByFilteringImage:originImage];_CIImage=[[CIImagealloc] 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];}}returnimage;}-(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, 畢竟我得在 AVCaptureVideoDataOutputSampleBufferDelegateAVCaptureAudioDataOutputSampleBufferDelegate 這兩個回撥做文章,為了滿足需求,所以得在不侵入 GPUImage 原始碼的前提下點功夫。

怎麼樣才能在不破壞 GPUImageVideoCamera 的程式碼呢?我想到兩個方法,第一個是建立一個類,然後把 GPUImageVideoCamera 裡的程式碼拷貝過來,這麼做簡單粗暴,缺點是若以後 GPUImage 升級了,程式碼維護起來是個小災難;再來說說第二個方法——繼承,繼承是個挺優雅的行為,可它的麻煩在於獲取不到私有變數,好在有強大的 runtime,解決了這個棘手的問題。下面是用 runtime 獲取私有變數:

Objective-C
123456 -(AVCaptureAudioDataOutput*)gpuAudioOutput{Ivarvar=class_getInstanceVariable([superclass],"audioOutput");idnameVar=object_getIvar(self,var);returnnameVar;}

至此取景框實現了濾鏡的渲染並保證了列表的滑動幀率。

2、實時合成以及 GPUImage 的 outputImageOrientation

顧名思義,outputImageOrientation 屬性和影象方向有關的。GPUImage 的這個屬性是對不同裝置的在取景框的影象方向做過優化的,但這個優化會與 videoOrientation 產生衝突,它會導致切換攝像頭導致影象方向不對,也會造成拍攝完之後的視訊方向不對。 最後的解決辦法是確保攝像頭輸出的影象方向正確,所以將其設定為 UIInterfaceOrientationPortrait,而不對 videoOrientation 進行設定,剩下的問題就是怎樣處理拍攝完成之後視訊的方向。

先來看看視訊的實時合成,因為這裡包含了對使用者合成的 CVPixelBufferRef 資源處理。還是使用繼承的方式繼承 GPUImageView,其中使用了 runtime 呼叫私有方法:

Objective-C
123456789 SELs=NSSelectorFromString(@"textureCoordinatesForRotation:");IMPimp=[[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 的處理程式碼:

Objective-C
12345678910111213141516171819202122232425262728293031323334 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(height,width));CGContextRefcgcontext=UIGraphicsGetCurrentContext();CGAffineTransformtransform=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 屬性設定如下:

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

經過這兩次方向的處理,合成的小視訊終於方向正常了。此處為簡版的合成視訊程式碼:

Objective-C
123456 CIImage*image=[[CIImagealloc] initWithCGImage:img.CGImage options:nil];CVPixelBufferLockBaseAddress(pixelBuffer,0);[self.context.CIContext render:image toCVPixelBuffer:pixelBuffer];...[_videoPixelBufferAdaptorappendPixelBuffer:pixelBufferwithPresentationTime: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。