開源一個上架 App Store 的相機 App
1、GLKView和GPUImageVideoCamera
一開始取景框的預覽我是基於 GLKView 做的,GLKView 是蘋果對 OpenGL
的封裝,我們可以使用它的回撥函式 -glkView:drawInRect:
進行對處理後的 samplebuffer
渲染的工作(samplebuffer
是在相機回撥 didOutputSampleBuffer
產生的),附上當初簡版程式碼:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 | -(CIImage*)renderImageInRect:(CGRect)rect{CMSampleBufferRefsampleBuffer=_sampleBufferHolder.sampleBuffer;if(sampleBuffer!=nil){UIImage*originImage=[self imageFromSamplePlanerPixelBuffer |
這樣的實現在低端機器上取景框會有明顯的卡頓,而且 ViewController 上的列表幾乎無法滑動,雖然手勢倒是還可以支援。 因為要實現分段拍攝與回刪等功能,採用這種方式的初衷是期望更高度的自定義,而不去使用 GPUImageVideoCamera
, 畢竟我得在 AVCaptureVideoDataOutputSampleBufferDelegate
, AVCaptureAudioDataOutputSampleBufferDelegate
這兩個回撥做文章,為了滿足需求,所以得在不侵入 GPUImage
原始碼的前提下點功夫。
怎麼樣才能在不破壞 GPUImageVideoCamera
的程式碼呢?我想到兩個方法,第一個是建立一個類,然後把 GPUImageVideoCamera
裡的程式碼拷貝過來,這麼做簡單粗暴,缺點是若以後 GPUImage
升級了,程式碼維護起來是個小災難;再來說說第二個方法——繼承,繼承是個挺優雅的行為,可它的麻煩在於獲取不到私有變數,好在有強大的 runtime,解決了這個棘手的問題。下面是用 runtime 獲取私有變數:
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 呼叫私有方法:
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 的處理程式碼:
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-C12 | _videoInput.transform=CGAffineTransformRotate(_videoConfiguration.affineTransform,-M_PI_2); |
經過這兩次方向的處理,合成的小視訊終於方向正常了。此處為簡版的合成視訊程式碼:
Objective-C123456 | 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。